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>
}
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>
}
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>
}
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")
Avoid conflicts:
const { state:mystore1 } = useStore("MyStore1")
const { state:mystore2 } = useStore("MyStore2")
Outside the "React Hooks":
const { state:mystore } = getStore("MyStore")
Then:
<div>{mystore.value}</div>
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)),
}
}
function MyComponent() {
const { getUsers } = useStore("myStore")
return getUsers("pi").map ( user => <div>{user.name}</div>)
}
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
}
},
}
}
function MyComponent() {
const { state, setValue1 } = useStore("myStore")
return <button onClick={e=>setValue1(state.value1+1)}>
value1: {state.value1}
</button>
}
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 }),
}
}
function MyComponent() {
const { state, fetch } = useStore("myStore")
return <button onClick={e=>fetch()}>
value1: {state.value}
</button>
}
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
Other link:
sandbox
template SPA
Top comments (0)