Simplify state with reducer
M. Akbar Nugroho
Posted on March 8, 2023
TL;DR
React state is good to store a reactive data. When the data changes, the UI will re-render and shwoing the latest update.
Complex UI also need more than one state. Possibly more than three.
To simplify manage the state, instead of updating the state one by one using its setter function, it's better to put all states in one place and update it according to event occured using dispatch()
function.
// Example of end result
// From this
setLoading(true);
setVisible(true);
setStatus('error');
// To this
dispatch({ type: 'loading_started' });
dispatch({ type: 'status_changed', payload: 'success' });
dispatch({ type: 'visible_toggled' });
Introduction
We love state because it makes our apps interactive with less of code, but managing lots of state can be tricky.
Imagine we have a form component. It's very common case that a form can have mutiple state inside of it. For example, a form may have states for loading, status, form submit, etc.
The reason why we do this because we want to tell the users what happen to the current process.
With so many states it is a good advice if you put all states inside one place. A large, maintainable and consistent shape.
And now, we are going to explore one of built-in React API to create our own shape to simplify managing the states.
The Distraction
A form commonly use an alert component to inform the users that their data is successfully processed or not.
An alert component appears with different style according to the status
state.
...and your code basically do something like this:
setStatus('success');
Theres nothing wrong with that code above. But, could you imagine managing many states inside a component? BTW, let's add more complexity to the UI.
Here's what the changes does. When user clicks the submit button, the button text change to "Loading...". Indicating that the data is being processed. The button is disabled to prevent user clicks multiple times until the process done.
So far we have two states. The status
and loading
. Can you spot the "distraction"? I think not yet.
Some particular apps using an overlay to display the form to minimize the page transition which will improve UX. The form should appear when user click a "trigger" and can be closed.
Now if we combining all states we should have status
, loading
and visible
.
Let's assume we have declared those states inside our component and to change the value for each state, you use the setter function.
setVisible(true);
setLoading(true);
setStatus('warning');
As your UI getting more complex, you will have more states. More states meaning more setter functions and that's the first distraction.
With so many setter functions, the behaviour of consistency to update the state a little bit lost because to update a state you need a specific setter function.
If using multiple setter function in a single component isn't make you distracted, let's take a look to maintainability.
Imagine you develop a text editor for eCommerce website. So the seller can format the description part of a product to provide better information.
From image above we possibly have four states inside the text editor. Which is:
formatter
mode
showOptions
letterCount
Although this is very simple text editor, you need to extract the UI into components based on its functionality and this is the problem come from.
// Just example :)
function TextEditor() {
return (
<div>
<Formatter />
<ModeSwitch />
<Options />
<ContentBody >
<LetterCount />
</div>
);
}
Some of components need sharing states to act normally. For example the <Formatter />
may use the mode
state because the formatter appear based on what current mode is being used. If user switch to Markdown, the formatter should disappear and vice versa.
The question is how do we share the states through all components?
The answer is very simple. Yes, using the Context. But, how? Like this?
function TextEditor() {
// state declarations here
return (
<div>
<TextEditorProvider
formatter={formatter}
mode={mode}
showOptions={showOptions}
letterCount={letterCount}
>
<Formatter />
<ModeSwitch />
<Options />
<ContentBody >
<LetterCount />
</TextEditorProvider>
</div>
);
}
And inside the <Formatter />
component, use the mode
state.
function Formatter() {
const state = useContext(TextEditorContext);
const [mode] = state.mode;
return mode === 'editor' <div /> : null;
}
Another problem is the props of <TextEditorProvider />
grow exponentially as you add more state to the <TextEditor />
component.
So, do we have better solution? Well, of course, yes.
The Flux Architecture
We are not going to discuss Flux as a whole because it's a huge topic. What we're gonna do is to getting know about the main idea of Flux Architecture.
Flux Architecture or Flux is a pattern to manage states that live inside your component.
It's not a framework, but something like idealogy similar to MVC pattern. Even though it has minimal library to simplify our work.
Flux has four main parts:
- Dispatcher
- Stores
- Actions
- Views
https://facebook.github.io/flux/docs/in-depth-overview
Dispatcher
A dispatcher is a central place to updating the state. It is similar with setter function of React state.
const [state, setState] = useState();
// This is the dispatcher.
setState();
Note:
That code above is not the actual flux dispatcher.
Stores
A store is what holds the data of an application. So, instead of expose the data publicly, it's wrap the data in the store and perform operation inside it (via dispatcher).
You can see here for an example.
Actions
An action is a simple object that describe of what condition or event that being occurred. Action is used by dispatcher to determine what operation needs to be performed.
const MODE_SWITCHED_TO_MARKDOWN = {
type: 'mode-switched',
payload: 'markdown'
};
const MODE_SWITCHED_TO_EDITOR = {
type: 'mode-switched',
payload: 'editor'
};
// Dispatch an action
dispatcher.dispatch(MODE_SWITCHED_TO_MARKDOWN);
If an action need to pass a data or you want the action dynamic, you can use Action Creator. Action Creator is a simple function that produce an action.
const switchMode = (mode) => ({ type: 'mode_switched', payload: mode })
dispatcher.dispatch(switchMode('html'));
We have to remember that the most important part is type
because we need this information to identify what operation should be performed.
The why Flux pattern can provide a better solution rather than raw React state because with Flux we can do many things. Not only holds a data.
- Separation of concerns
- Easier Debugging
- Code predictability
The list above is some of Flux benefits.
Views
Nothing special with views. Views is component you made that may use the store or dispatch an action.
// Example of component that dispatch an action
function ModeSwitch() {
return <Switcher
onSwitch={() => {
dispatcher.dispatch(MODE_SWITCHED_TO_EDITOR);
}}
/>
}
See? Now it looks maintainable and consistent because we don't need to use specific setter function for each state.
You might be feel this approach little bit confused because I only show the main idea and use part of the code. But, once you got the main idea I ensure you will love this approach.
And the reason why I only show the main idea because Flux is currently in maintenance mode, but the idea still live and adopted by React.useReducer
and Redux
.
To get more information about Flux, go to their official website here.
Combining Context And Flux
Flux by defult is shareable between all views. That means we don't need to pass the Flux data into the Context.
Since Flux is in maintenance mode, it's not good practice to use Flux anymore. But, follow the main idea is a good deal, tho.
Note:
The maintenance mode is refers to Flux library not the main idea.
Is it possible to implement the Flux idea and share it between components through Context? The answer is yes.
Let's implement the Context first.
// text-editor-context.jsx
import React from 'react';
const TextEditorContext = React.createContext({});
export const TextEditorProvider = (props) => {
<TextEditorContext.Provider value={{ store: props.store, dispatch: props.dispatch }}>
{props.children}
</TextEditorContext.Provider>
};
export default TextEditorContext;
The Context role is act like a bridge that connects all components to consume shareable state. Just it.
So, how implement the Flux? since the library itself is not continued by the maintainer.
Therefore, please welcome...
React.useReducer
What is React.useReducer
? Well, because you already know about Flux. It is simplified Flux without shareable ability. Therefore, we use Context to make it shareable.
React.useReducer
or simplified called reducer is slightly different compared to Flux.
Flux is require you to create a class that extends ReduceStore
from the Flux library. While React.useReducer
is a hook and takes two arguments to operates. The reducer
and inititalState
.
Note:
Read more about React.useState here.
With React.useState
we can create a Flux-like state and should answer the question from the previous section.
// Example of reducer.
const initialState = {
formatter: null,
mode: 'editor',
showOptions: false,
letterCount: 0,
};
// Actions
export const switchMode = (mode) => {
return {
type: 'mode-switched',
payload: mode,
};
};
// Rest of actions...
const reducer = (state, action) => {
switch (action.type) {
case 'mode-switched':
return { ...state, mode: action.payload }
case 'formatter-changed':
return { ...state, formatter: action.payload };
case 'options-toggled':
return { ...state, showOptions: !state.showOptions };
case 'letter-count-increased':
return { ...state, letterCount: state.letterCount + 1};
case 'letter-count-decreased':
return { ...state, letterCount: state.letterCount - 1};
default:
throw new TypeError('Unidentified Type Given.');
}
}
function TextEditor() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<TextEditorProvider state={state} dispatch={dispatch}>
<Formatter />
<ModeSwitch />
<Options />
<ContentBody >
<LetterCount />
</TextEditorProvider>
</div>
);
}
And now all components wrapped inside <TextEditorProvider />
should be able to access the state
object and dispatch
API.
The Final Result
After all, we are going to combine all solutions.
The inconsistency problem has been solved from code above. Theres no specific setter function. Theres only dispatch()
function to update specified state.
The maintainability problem is also solved because if you want add new feature or states, you are not polluting the <TextEditorProvider />
.
Instead of adding props, you just need to update the initialState
and adding some actions or action creators.
And now you can use the states across all components...
function ModeSwitch() {
const { dispatch } = useContext(TextEditorContext);
return <Switcher
onSwitch={() => {
dispatch(switchMode('markdown'));
}}
/>
}
// Theres no need to destruct the array :)
function Formatter() {
const { state } = useContext(TextEditorContext);
return state.mode === 'editor' <div /> : null;
}
You can even make the context simpler using a custom hook.
const useStore = () => {
const store = useContext(TextEditorContext);
return store;
};
// Use it like this
const { state, dispatch } = useStore();
Conclusion
Managing state can be tricky for complex application. Using combination of state and context is good, but we have better solution by using reducer.
Reducer is ensure us to update the state only using the dispatch()
API. Minimizing distraction when using setter function, improve maintainability and consistency.
The Flux pattern is every useful for large application. Separation of concern, easy to debug and predictability. All of those features are needed by long-runnung application.
Luckly, reducer is already implement this pattern. Even though with simplification, but it still worth.
By combining context and reducer, you got Flux with simpler approach. Awesome!
FIY, one of top library that use Flux idea is Redux. And this is my favorite React state management.
Posted on March 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024