This post is a brother of Full State Management in React (without Redux).
The thing is we define a local state for each component, through the use of useReducer
hook, then we mount up a tree of all the local states of all the components instantiated, and then make available that store through the use of useContext
in React and provide
-inject
api in Vue.
Because in Vue we don't have a useReducer
hook, we must do a simple equivalent one.
In this way we achieve a total control of the state in the tree.
The useReducer hook
Let's start by a simple equivalent of the useReducer
hook in React, but for Vue. This will be the code:
import { reactive } from 'vue'
export const useReducer = (reducer, iState) => {
const state = reactive(iState)
const dispatch = (action) => {
reducer(state, action)
}
return [state, dispatch]
}
You see it's quite simple. When defining the initial state in a separate file for passing it to the useReducer
function we must take care to define a function that returns each time (each invocation) a new object representing the initial state. If not, two instances of the same component will end up sharing the same state. Like this:
export const iState = () => ({
counter: 0,
})
Then, in the setup
function of the composition API we do this:
setup(props) {
const [state, dispatch] = useReducer(reducer, iState())
The reducer
function
There is a difference in the definition of the reducer function respect to the one we do in React.
This is the reducer for this app:
export const reducer = (state, action) => {
switch (action.type) {
case INCREASE:
state.counter++
break
}
}
As you can see we mutate directly the object and don't create a new one because if we did that we will lose reactivity.
Passing information up to the component tree
The technique used to pass information from down to up is using a HOC to provide extra properties to the component, which are catched
and infoRef
.
catched
is the callback passed to the child from where we want to get (catch) information, and infoRef
is where we will store that information.
This is the HOC:
import { ref } from 'vue'
export default (C) => ({
setup(props) {
const infoRef1 = ref(null)
const infoRef2 = ref(null)
const infoRef3 = ref(null)
const infoRef4 = ref(null)
const catched1 = (info) => (infoRef1.value = info)
const catched2 = (info) => (infoRef2.value = info)
const catched3 = (info) => (infoRef3.value = info)
const catched4 = (info) => (infoRef4.value = info)
return () => {
return (
<C
catched1={catched1}
catched2={catched2}
catched3={catched3}
catched4={catched4}
infoRef1={infoRef1}
infoRef2={infoRef2}
infoRef3={infoRef3}
infoRef4={infoRef4}
{...props}
/>
)
}
},
})
If you need more catched
and infoRef
s you can define them on this HOC as many as the maximum number of children a parent will have in the app.
As you can see we provide to the component with extra properties catched1
, catched2
, etc. The same for infoRef
.
How do we use it?
Let's look at the use of it in the component definitions. First, let's stipulate the structure of the app, of the tree. We will have to component definitions, App
and Counter
. App
will instantiate two Counter
s, while Counter
does not have any child.
Let's look at the definition of the App
component:
import { provide, reactive, ref, inject } from 'vue'
import Counter from '../Counter'
import styles from './index.module.css'
import withCatched from '../../hocs/withCatched'
import * as counterActions from '../Counter/actions'
import { iState, reducer } from './reducer'
import { useReducer } from '../../hooks/useReducer'
export default withCatched({
props: ['catched1', 'infoRef1', 'catched2', 'infoRef2'],
setup(props) {
const [state, dispatch] = useReducer(reducer, iState)
const name1 = 'counter1'
const name2 = 'counter2'
provide('store', {
state,
dispatch,
[name1]: props.infoRef1,
[name2]: props.infoRef2,
})
const store = inject('store')
const clicked1 = () => {
store[name1].value.dispatch(counterActions.increase())
}
const clicked2 = () => {
store[name2].value.dispatch(counterActions.increase())
}
return () => {
return (
<div className={styles.some}>
<Counter catched={props.catched1} name={name1} />
<Counter catched={props.catched2} name={name2} />
{store[name1].value && store[name1].value.state.counter}
{store[name2].value && store[name2].value.state.counter}
<button onClick={clicked1}>increase1</button>
<button onClick={clicked2}>increase2</button>
</div>
)
}
},
})
You can see how we use named components, that's it, we pass a property name
to each instance of Counter
in the App
component.
Now, let's look at the definition of the Counter
component:
import { onMounted, reactive, ref, inject, onUpdated } from 'vue'
import styles from './index.module.css'
import { useReducer } from '../../hooks/useReducer'
import { reducer, iState } from './reducer'
export default {
props: ['catched', 'name'],
setup(props) {
const [state, dispatch] = useReducer(reducer, iState())
onMounted(() => {
props.catched.bind(null, { state, dispatch })()
})
const store = inject('store')
return () => {
return (
<div class={styles.general}>
{store[props.name].value && store[props.name].value.state.counter}
</div>
)
}
},
}
Pay attention to this:
onMounted(() => {
props.catched.bind(null, { state, dispatch })()
})
This is how we uplift information to the parent component. In this case, we are sending up state
and dispatch
, but we can uplift any information we need to.
Conclusion
So that's it. This is how we can have perfect control of state
and dispatch
of all the components instantiated in the tree.
This is the final result:
As you can see the two counters are incremented individually.
Top comments (0)