[DIY] State Management - React Vs. Vue π
February 5th, 2021 | 10 mins readPublished on dev.to and medium.com
Years pass, technologies evolve, but some development patterns stay strong, improving over time... This is the case with State Management!
Introduced with Flux, an architecture designed by Facebook for Facebook, State Management is now a must for Web development. This development paradigm is mainly characterized by a one-way data flow; instead of the bidirectional data binding used by MVW frameworks such as AngularJS, or more recently Vue. Flux is created to solve some mistakes of the MVVM (Model-View / View-Model) pattern, especially when scaling up Web applications. With the creation of the concept of "store", there is no more (or at least less) problem linked to different data sources. Everything is centralized in the same place. It's said that the store is single source of truth!
Just like callback functions, promises or streams, State Management is a "reactive" programming paradigm. The idea of such a development is that components using the store data, react when these same data are updated. For this, State Management respects several rules:
- The store is read-only
- The data are updated by a "dispatcher"
- The dispatcher is solicited by actions
- User interface triggers actions
According to Flux implementations, the notion of "dispatcher" is more or less explicit, however the data flow stay the same: actions are dispatched by the view that update the store data and implicitly update views associated to this data. In the Web development world, there are many distinct implementations of Flux: Fluxxor, MobX, Overmind, etc... Redux and Vuex are respectively the more known for React and Vue ecosystems.
Although these dependencies are extremely practical and greatly facilitate the developer work, it's possible to build your State Management architecture. This is what brings us to this post!
Below, we'll see how to code your own Redux / Vuex, step by step, using latest versions of React and Vue tools currently available (version 17 for the library of Facebook, and version 3 for the community framework).
NB: React uses hooks, and Vue uses the Composition API. These last features being very similar, it will be interesting to see how they interface themselves in this kind of development.
The implementation of a State Management (whether in React or in Vue) is divided into two parts:
- The Provider who initializes the store engine
- Consumers who interact with the store, reading /
writing"dispatching" actions
Part 1 - The Provider
The creation of a store with the Facebook library is achieved by a clever combination of hooks and the Context API. Creating a context gives access to the <Provider />
component which will integrate the store data previously provided by the useReducer()
hook. Indeed, the "State - Reducer" development pattern plays an important role in the management of a complex state of a component.
import { createContext, useReducer } from 'react';
const initialState = {};
export const store = createContext(initialState);
const { Provider } = store;
const rootReducer = (state, { type, payload }) => {
switch (type) {
case 'SET_FIELD':
return {
...state,
[payload.key]: payload.value
};
case 'RESET_FIELD':
return {
...state,
[payload]: undefined
};
default:
throw new Error();
}
};
function StateProvider({ children }) {
const [state, dispatch] = useReducer(rootReducer, initialState);
return <Provider value={{ state, dispatch }}>{children}</Provider>;
}
export default StateProvider;
These few lines are enough to set up a store engine. However, to spread data (from a context) to child components, these same components must be encapsulated by the parent component (<StateProvider />
), preferably at the highest level of the application.
import StateProvider from './StateProvider';
import StateConsumer from './StateConsumer';
function App() {
return (
<StateProvider>
<StateConsumer />
</StateProvider>
);
}
export default App;
For the community framework, with Vue's version 3, the store initialization is mainly based on the Composition API, as well as on the "Provide / Inject" development pattern. This last feature (already present in Vue 2) is very similar to React's Context API, and allows to extend global data to a whole part of the application.
<template>
<slot />
</template>
<script>
import { reactive, provide, readonly } from 'vue';
export default {
name: 'StateProvider',
setup() {
const state = reactive({});
provide('STATE', readonly(state));
const setField = (key, value) => {
state[key] = value;
};
const resetField = key => {
state[key] = undefined;
};
provide('SET_FIELD', setField);
provide('RESET_FIELD', resetField);
}
};
</script>
Above (functions speaking for themselves) we quickly notice that we declare a reactive variable (the application global state), then we make it available, as well as functions allowing to mutate this variable. Then (and just like React), it isn't enough to inject the store data into child components to interact with this last one, it's also necessary to wrap these same components by the parent component (<StateProvider />
again), responsible for the store.
<template>
<StateProvider>
<StateConsumer />
</StateProvider>
</template>
<script>
import StateProvider from './StateProvider';
import StateConsumer from './StateConsumer';
export default {
name: 'App',
components: {
StateProvider,
StateConsumer
}
};
</script>
Part 2 - The Consumer
NB: In the rest of this post, CSS classes that you'll find in the rendering of <StateConsumer />
components come from an UI framework: Bulma!
Once the child component is encapsulated by the store owner component, we retrieve its data using the inject()
function with the Vue framework. The parameter of this function is simply a unique identifier, which refers to the variable / function previously provided by the parent component.
<template>
<div class="columns">
<div class="column">
<div class="field">
<div class="label">FullName *</div>
<div class="control">
<input
class="input"
:value="state['fullName'] || ''"
@input="$event => setField('fullName', $event.target.value)" />
</div>
</div>
</div>
<div class="column">
<button
class="button"
@click="() => resetField('fullName')">
Reset
</button>
</div>
</div>
</template>
<script>
import { inject } from 'vue';
export default {
name: 'StateConsumer',
setup() {
const state = inject('STATE');
const setField = inject('SET_FIELD');
const resetField = inject('RESET_FIELD');
return {
state,
setField,
resetField
};
}
};
</script>
The setup()
option will transmit the store state, as well as functions to update it, to the child component <StateConsumer />
before mounting it. In the template above, we use directly state.fullName
value of the store, and we update it when the onchange
event is triggered by the <input>
, or when the onclick
event is played by the <button>
.
On the React library side, store's values (i.e. its state, and the dispatch()
function) are retrieved through another hook: useContext()
. By importing the store context, and passing it as a parameter of this function, a "stateless" component "connect" (refers to Redux) to the application store.
import { useContext } from 'react';
import { store } from './StateProvider';
function StateConsumer() {
const { state, dispatch } = useContext(store);
const setField = (key, value) => dispatch({ type: 'SET_FIELD', payload: { key, value } });
const resetField = key => dispatch({ type: 'RESET_FIELD', payload: key });
return (
<div className="columns">
<div className="column">
<div className="field">
<div className="label">FullName *</div>
<div className="control">
<input
className="input"
defaultValue={state['fullName'] || ''}
onChange={e => setField('fullName', e.target.value)}
/>
</div>
</div>
</div>
<div className="column">
<button
className="button"
onClick={() => resetField('fullName')}>
Reset
</button>
</div>
</div>
);
}
We still have to update the store... To do this, simply dispatch an action. By convention, an action is an object with two properties:
- The "type" used as a reference for the dispatcher
- The "payload" used by the store to update its state
Hooks Vs. Composition API
The introduction of hooks with React 16.8 and the appearance of Vue 3's Composition API are changing the way we use the store. Already present since version 7.1.0 of the "React-Redux" dependency, hooks (useSelector()
/ useDispatch()
) greatly facilitate the "connection" with the store, and avoid a HOC (High Order Component) process, to pass some data from a parent component to properties of a child component. Vue's Composition API can be used very similar to React hooks.
import { useContext } from 'react';
import { store } from './StateProvider';
export default function useField(key) {
const { state, dispatch } = useContext(store);
const setField = key => value => dispatch({ type: 'SET_FIELD', payload: { key, value } });
const resetField = key => () => dispatch({ type: 'SET_FIELD', payload: key });
return [state[key] || '', setField(key), resetField(key)];
}
import { inject, computed } from 'vue';
export default function useField(key) {
const state = inject('STATE');
const setField = inject('SET_FIELD');
const resetField = inject('RESET_FIELD');
const setFieldByKey = key => value => setField(key, value);
const resetFieldByKey = key => () => setField(key);
return [computed(() => state[key] || ''), setFieldByKey(key), resetFieldByKey(key)];
}
This way of doing things is more and more widespread in Web developments, and responds to the following principle: split to rule better; Perfect for applications with more than 100 components...
NB: Conventionally, the name of this kind of function should start with "use" to specify that it's a composition function / custom hook.
This concept is rather intelligent, and allows us to think about our applications more finely, brick by brick. This promotes the code reusability for components having the same logic: the same way of reading the store and / or updating all or part of the store.
- import { useContext } from 'react';
- import { store } from './StateProvider';
+ import useField from './hooks/useField';
function StateConsumer() {
- const { state, dispatch } = useContext(store);
+ const [fullName, setFullName, resetFullName] = useField('fullName');
- const setField = (key, value) => dispatch({ type: 'SET_FIELD', payload: { key, value } });
- const resetField = key => dispatch({ type: 'RESET_FIELD', payload: key });
return (
<div className="columns">
<div className="column">
<div className="field">
<div className="label">FullName *</div>
<div className="control">
<input
className="input"
- defaultValue={state['fullName'] || ''}
+ defaultValue={fullName}
- onChange={e => setField('fullName', e.target.value)}
+ onChange={e => setFullName(e.target.value)}
/>
</div>
</div>
</div>
<div className="column">
- <button className="button" onClick={() => resetField('fullName')}>
+ <button className="button" onClick={resetFullName}>
Reset
</button>
</div>
</div>
);
}
<template>
<div class="columns">
<div class="column">
<div class="field">
<div class="label">FullName *</div>
<div class="control">
<input
class="input"
- :value="state['fullName'] || ''"
+ :value="fullName"
- @input="$event => setField('fullName', $event.target.value)"
+ @input="$event => setFullName($event.target.value)"
/>
</div>
</div>
</div>
<div class="column">
- <button class="button" @click="() => resetField('fullName')">
+ <button class="button" @click="resetFullName">
Reset
</button>
</div>
</div>
</template>
<script>
- import { inject } from 'vue';
+ import useField from './composition/useField';
export default {
name: 'StateConsumer',
setup() {
- const state = inject('STATE');
- const setField = inject('SET_FIELD');
- const resetField = inject('RESET_FIELD');
+ const [fullName, setFullName, resetFullName] = useField('fullName');
return {
- state,
- setField,
- resetField
+ fullName,
+ setFullName,
+ resetFullName
};
}
};
</script>
NB: The advantage of the above function is that it directly handles the default value of the "field" if its value isn't (yet) present in the store; instead of handling it in the component template.
Conclusion
I've been using State Management for several years now, often with Redux (in parallel with Vuex), I've learned to know how it works and its many benefits.
While extremely practical, State Management makes sense in a scale Web application, with a multitude of components, as well as multiple features. This makes it easier to centralize, read and update data.
Latest versions of JavaScript frameworks / libraries leads us to more finely decompose our applications. The use of hooks / Composition API makes State Management more accessible and transparent (no need for additional dependencies mystifying part of the code). So today I sometimes use this development logic on a smaller scale (to build Single Page Apps with less than 50 components for example).
You'll understand, I'm quite a fan of reactive programming through the store usage. If you develop regularly with Redux (or Vuex), or even other libraries (RxJS), I invite you to do this exercise of creating a State Management from scratch (for code's sake π).
Finally, this comparison between React and Vue, makes it possible to realize that these two frameworks revolving around a Virtual DOM aren't so far from each other, despite their own concepts (such as Vue's "Provide / Inject" pair). After this post, I think I reiterated this work on other tools, probably Svelte whose some concepts are quite close to React and Vue. That will likely result in a spin-off post...
Sources
About this post, I made two demonstration projects (visually identical) by applying the architecture explained above. Here are links: