Simpla Store

July 25, 2022 · View on GitHub

The most simple usecase is as a store container. This is not supported observe detailed with message pattern. Looks very simple and easy to handle, but as it larger and becomes more complex, it can break the order.

Store Overview

Let's take a look at the completed store first. Dig into step by step from there.

import { LiteStore } from "memento.react"

const delay = (timeout: number) =>
    new Promise(resolve => setTimeout(resolve, timeout))

const simpleState = {
    count: 0,
    history: [],
    isLoading: false,
}

@meta({ name: "SimpleStore" })
export class SimpleStore extends LiteStore<typeof simpleState> {
    constructor() {
        super(simpleState)
    }

    async countUpAsync() {
        this.setState({
            ...this.state,
            isLoading: true,
        })

        await delay(500)

        const state = this.state
        const count = state.count + 1

        this.setState({
            ...state,
            history: [...state.history, count],
            count,
            isLoading: false,
        })
    }

    setCount(num: number) {
        this.setState({
            ...this.state, count: num,
            history: [...this.state.history, num]
        })
    }

    async delay(timeout: number) {
        await new Promise(resolve => setTimeout(resolve, timeout))
    }
}

State

You should define state to manage in store before create store. T with LiteStore<T> is the state type, plane object or class instance extended State<T>. State can be specified as plane object, but State<T> provides useful methods for immutable state.

Plane Object

const simpleState = {
    count: 0,
    history: [],
    isLoading: false,
}

Extendsd state

import { State } from "memento.core"

/**
 * State for counter.
 */
export class CounterState extends State<CounterState> {
    count = 0
    test = "foo"

    get next() {
        return this.count + 1
    }
}

The State<T> behavior is following.


const state = new CounterState()
console.log(state) // { count: 0, test: "foo" }

const state2 = state.clone({ count: 5 })
console.log(state2) // { count: 5,test: "foo" }

const state3 = state2.clone({
    count: state2.count * 3,
    test: "bar"
})
console.log(state3) // { count: 15, test: "bar" }

Define Store

Create class and extends LiteStore and specify initialState to super on constructor. Specify initial state into args of super constructor

export class SimpleStore extends LiteStore<typeof simpleState> {
    constructor() {
        super(simpleState)
    }
}

or

export class SimpleStore extends LiteStore<CounterState> {
    constructor() {
        super(new CounterState())
    }
}

Store Name

Specify store name with @meta decorator following

@meta({ name: "SimpleStore" })
export class SimpleStore extends LiteStore<...> {

}

If js or you do not want to use decorator. You should define static member following.

export class SimpleStore extends LiteStore<...> {
    static storeName = "SimpleStore"
}

State changes

To change the state, define a method as an action in the store. Change the state by calling it from the outside.

  • Call this.setState(state:T) to update store state

The following example defines countUpAsync.

State change steps

  • Loading state to true
  • Run async timer
  • Set the final state
async countUpAsync() {
    this.setState({
        ...this.state,
        isLoading: true,
    })

    await delay(500)

    const state = this.state
    const count = state.count + 1

    this.setState({
        ...state,
        history: [...state.history, count],
        count,
        isLoading: false,
    })
}

Set state directory in setCount. update is public method, so you can call from outside of store class.

setCount(num: number) {
    this.setState({
        ...this.state, count: num,
        history: [...this.state.history, num]
    })
}

Register and Create Store

You finished to define store, call createProvider and specify YourStore to stores as array.

export const provider = () => createProvider({
    stores: [SimpleStore],
})

With React App

memento.react includes a <StoreProvider /> component, which makes the store available to the rest of your app. Specify provider context to StoreProvider as react context provider.

import { 
    StoreProvider,
} from "memento.react"
import React,  {useState } from "react"
import {SimpleStoreView} from "./SimpleStoreView"

const App = () => {
    const [provider] = useState(() => provider())

    return (
        <StoreProvider provider={provider}>
            <SimpleStoreView />
        </StoreProvider>
    )
}

Hooks​

memento.react provides a pair of custom React hooks that allow your React components to interact with the store. class api is not supported.

useObserver reads a value from the store state and subscribes to updates, while useDispatch returns the store's dispatch method to let you dispatch actions defined in store.

import { 
    useObserver,
    useDispatch,
} from "memento.react"
import React,  {useState } from "react"

export const SimpleStoreView = () => {dedux
    // store state
    const counter = useObserver(SimpleStore)
    const dispatch = useDispatch(SimpleStore)
    // store state with sliced by selector
    const history = useObserver(SimpleStore, state => state.history)

    // local state
    const [toAdd, setToAdd] = useState(1)

    const handleClick = () => {
        dispatch(store => store.countUpAsync())
    }

    const handleSet = () => {
        dispatch(store => store.setCount(counter.count + toAdd))
        setToAdd(1)
    }

    return (
        <div style={{
            display: "flex",
            flexDirection: "column",
            alignItems: "center"
        }}>
            <h2>Counter</h2>

            
            <p>{counter.count}</p>

            <button onClick={handleClick}>
                Count UP
            </button>

            <div style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                width: "260px"
            }}>
                <p>{counter.count}</p>
                <div style={{ flex: "1 1 auto" }} />
                <p>+</p>
                <div style={{ flex: "1 1 auto" }} />
                <input
                    type="number"
                    value={toAdd}
                    onChange={e => setToAdd(Number(e.target.value))}
                />
                <button onClick={handleSet}>
                    Calc
                </button>
            </div>

            <h4>Loading : {counter.isLoading ? "YES" : "NO"}</h4>

            <h4>History</h4>
            <div
                style={{
                    height: "300px",
                    width: "400px",
                    overflowY: "auto",
                    background: "rgba(0,0,0,0.08)",
                    textAlign:"center",
                }}
            >
                {
                    !history.length && <p>No history</p>
                }
                {
                    history.map(h =>
                        <p key={h}>history: {h}</p>
                    )
                }
            </div>
        </div>
    )
}

Other hooks

useRef always refelences currrent state.

Example

const ref = useRef(SimpleStore)

useEffect(( => {
    console.log(ref.state) // output current state
}), [anyState1, anyState2])

useStore provides the store.

Example

const simpleStore: SimpleStore = useStore(SimpleStore)

useEffect(( => {
    simpleStore.invokeAnyAction() // output current state
}), [])

useStore provides a store provider of app.

Example

const provider = useProvider()

useEffect(( => {
    console.log(provider.rootState()) // output root state
}), [])

API Refelences

API Refelences

NEXT

Next, let's see more larger and complex pattern with FluxStore.

FluxStore