DEV Community

Cover image for React clean with the STOREs
priolo22
priolo22

Posted on

React clean with the STOREs

React is easy to use to create the "VIEW".
But when the application grows ... it's not enough!
Passing variables and methods in "props"
the code turns into leaves entangled in the nodes of the VIEW tree!

A practical example:

import { useState } from "react"

// main with "data"
export default function App() {
  const [data, setData] = useState(0)
  return (
    <div className="App">
      <ShowDataCmp data={data} />
      <ContainerCmp data={data} onChange={setData} />
    </div>
  )
}

// render data
function ShowDataCmp({ data }) {
  const renderData = `Data: ${data}`
  return <div>{renderData}</div>
}

// simple container
function ContainerCmp({ data, onChange }) {
  return <div style={{ background: "blue", padding: "5px" }}>
    <ChangeDataCmp data={data} onChange={onChange} />
  </div>
}

// component for change data
function ChangeDataCmp({ data, onChange }) {
  const handleOnClick = (e) => {
    const newData = data + 1
    onChange(newData)
  }
  return <button onClick={handleOnClick}>Change Data</button>
}
Enter fullscreen mode Exit fullscreen mode

sandbox

Frame 3

Code and data are mixed in the VIEW.
If the application grows, you won't understand where the data and methods come from.
Let's face it: it's real shit!


Context

The Context is React's "native" solution.

Reworking the previous example we get:

import { createContext, useContext, useState } from "react"

const Context = createContext()

// main with "data"
export default function App() {
  const reducer = useState(0)
  return (
    <div className="App">
      <Context.Provider value={reducer}>
        <ShowDataCmp />
        <ContainerCmp />
      </Context.Provider>
    </div>
  )
}

// render data
function ShowDataCmp() {
  const reducer = useContext(Context)
  const renderData = `Data: ${reducer[0]}`
  return <div>{renderData}</div>
}

// simple container
function ContainerCmp() {
  return <div style={{ background: "blue", padding: "5px" }}>
    <ChangeDataCmp />
  </div>
}

// component for change data
function ChangeDataCmp() {
  const reducer = useContext(Context)
  const handleOnClick = (e) => {
    const newData = reducer[0] + 1
    reducer[1](newData)
  }
  return <button onClick={handleOnClick}>Change Data</button>
}
Enter fullscreen mode Exit fullscreen mode

sandbox

Frame 4

Not bad! But there are two problems:

  • We have to create CONTEXT and STATE for each STOREs. If there were many STOREs the complexity would increase.
  • It is not clear how to divide the BUSINESS LOGIC from the VIEW

STOREs

There are tons of LIB out there!
If you want to stay light use JON
it's just a little sugar on "Native Providers"
... and heavily influenced by VUEX

Our example could be:

import { MultiStoreProvider, useStore } from "@priolo/jon"

const myStore = {
  // lo stato iniziale dello STORE
  state: {
    counter: 0
  },
  getters: {
    // 
    renderData: (state, _, store) => `Data: ${state.counter}`
  },
  actions: {
    increment: (state, step, store) => {
      store.setCounter(state.counter + step)
    }
  },
  mutators: {
    setCounter: (state, counter, store) => ({ counter })
  }
}

// main with "data"
export default function App() {
  return (
    <MultiStoreProvider setups={{ myStore }}>
      <div className="App">
        <ShowDataCmp />
        <ContainerCmp />
      </div>
    </MultiStoreProvider>
  )
}

// render data
function ShowDataCmp() {
  const { renderData } = useStore("myStore")
  return <div>{renderData()}</div>
}

// simple container
function ContainerCmp() {
  return (
    <div style={{ background: "blue", padding: "5px" }}>
      <ChangeDataCmp />
    </div>
  )
}

// component for change data
function ChangeDataCmp() {
  const { increment } = useStore("myStore")
  const handleOnClick = (e) => increment(1)
  return <button onClick={handleOnClick}>Change Data</button>
}
Enter fullscreen mode Exit fullscreen mode

sandbox

Frame 5

state

The initial STATE of the STORE. "Single Source of Truth"
The STATE is connected to the VIEW (via React):
When the STATE changes then the VIEW updates automatically.

To access the STATE of a STORE:

const { state } = useStore("MyStore")
Enter fullscreen mode Exit fullscreen mode

Avoid conflicts:

const { state:mystore1 } = useStore("MyStore1")
const { state:mystore2 } = useStore("MyStore2")
Enter fullscreen mode Exit fullscreen mode

Outside the "React Hooks":

const { state:mystore } = getStore("MyStore")
Enter fullscreen mode Exit fullscreen mode

Then:

<div>{mystore.value}</div>
Enter fullscreen mode Exit fullscreen mode

getters

Returns a value of the STATE.
Although you can access the STATE directly
in many cases you will want some processed data.

For example: a filtered list:

const myStore = {
   state: { 
       users:[...] 
       }, 
   getters: {
      getUsers: ( state, payload, store ) 
         => state.users.filter(user=>user.name.includes(payload)),
   }
}
Enter fullscreen mode Exit fullscreen mode
function MyComponent() {
   const { getUsers } = useStore("myStore")
   return getUsers("pi").map ( user => <div>{user.name}</div>)
}
Enter fullscreen mode Exit fullscreen mode

The signature of a getter is:

  • state: the current value of the STATE
  • payload: (optional) the parameter passed to the getter when it is called
  • store: the STORE object itself. You can use it as if it were "this"

GETTERS should ONLY "contain" STATE and GETTERS


mutators

The only way to change the STATE.
It accepts a parameter and returns the "part" of STORE to be modified.

For example:

const myStore = {
   state: { 
       value1: 10,
       value2: "topolino",
    }, 
   mutators: {
      setValue1: ( state, value1, store ) => ({ value1 }),
      // ! verbose !
      setValue2: ( state, value, store ) => { 
          const newValue = value.toUpperCase()
          return {
              value2: newValue
          }
      },
   }
}
Enter fullscreen mode Exit fullscreen mode
function MyComponent() {
    const { state, setValue1 } = useStore("myStore")
    return <button onClick={e=>setValue1(state.value1+1)}>
        value1: {state.value1}
    </button>
}
Enter fullscreen mode Exit fullscreen mode

the signature of a mutator is:

  • state: the current value of the STATE
  • payload: (optional) the parameter passed to the mutator when it is called
  • store: the STORE object itself. You can use it as if it were "this"

Inside MUTATORS you should use ONLY the STATE.


actions

Contains the business logic
ACTIONS can be connected to SERVICEs and APIs
They can call STATE values, MUTATORS and GETTERS
They can be connected to other STOREs
They can be async

A typical use:

const myStore = {
    state: { 
        value: null,
    }, 
    actions: {
        fetch: async ( state, _, store ) => {
            const { data } = await fetch ( "http://myapi.com" )
            store.setValue ( data )
        }
    },
    mutators: {
        setValue: ( state, value, store ) => ({ value }),
    }
}
Enter fullscreen mode Exit fullscreen mode
function MyComponent() {
    const { state, fetch } = useStore("myStore")
    return <button onClick={e=>fetch()}>
        value1: {state.value}
    </button>
}
Enter fullscreen mode Exit fullscreen mode

the signature of a action is:

  • state: the current value of the STATE
  • payload: (optional) the parameter passed to the action when it is called
  • store: the STORE object itself. You can use it as if it were "this"

Conclusion

JON is designed to be VERY LIGHT and integrated with React.
Basically it is a utility to use native PROVIDERS
You can easily see them in the browser tool
screenshot1

Other link:
sandbox
template SPA

Top comments (0)