Requirements
In this article we will assume that you know how React works on a basic level and that you know how Typescript works on a basic level, if you have been looking for a new way to improve the way you manage the state of your application, stay in this article and you will learn a great way to do it.
Introducción y Motivación ✍️
State management in React has been evolving in recent years, the main solutions of the community are based on the architecture pattern known as Flux, if you are in the world of React for a while you have heard or worked with Redux, This solution was the most popular and was practically a standard for a long time, however the arrival of hooks facilitated the ability to unlink from libraries to achieve efficient and scalable management of the application state, this is the main motivation for writing this I will show how to implement Flux without the need for any library using React's useContext
and useReducer
hooks.
Before continuing as I mentioned, there is a library that already solves this, if yours is more to go to the side of the libraries for state management, here are some of the most popular options, and a link so that you have a complete list since there are many alternatives.
Zustand: https://github.com/pmndrs/zustand (my favorite)
Redux: https://redux.js.org/
RecoilJs: https://recoiljs.org/
HookState: https://hookstate.js.org/
XState: https://xstate.js.org/docs/
https://openbase.com/categories/js/best-react-state-management-libraries?orderBy=RECOMMENDED&
Flux - Architecture Pattern
Every software developer must solve problems to situations that arise in each project that we carry out, the solutions that we devise in the best scenario can be reused and become a solution that many other developers implement in their codes, these solutions are known as programming patterns, there are many and of various types, this is not an article to go into detail with this, but it is necessary to understand that Flux was born as a software architecture pattern mainly for FrontEnd and comes to solve certain problems that existed in other previous patterns such as the well-known MVC.
To get into the main details of this architecture, I present the protagonists of a Flux architecture:
View: The view, as its name indicates, corresponds to the visual component that is responsible for interacting with the users of the system, in the views it is from where the actions will be triggered.
Action: Actions are simply objects that indicate the action to be executed and can contain data if necessary.
Dispatcher: This is in charge of taking the action and sending it to the store in charge by executing it.
Store: This is a container that will store the data or the state of the application that is sent in the actions, Flux accepts several Stores, this can be confusing for those who learned Redux without learning Flux since one of its principles is a single Store.
One of the main characteristics of Flux is that it proposes to handle the data flow in a unidirectional way, to understand it better take a look at the following Flow.
In the flow we see that all action is directed to a dispatcher, this takes it to the store and the store returns it to the view, if they see that there are some actions that do not come from the view, these can be initial data of the app that not necessarily come from sight.
Understanding Flux is important, this is to understand in depth how some libraries like Redux have adopted this architecture, however in some cases they adapt it to what they consider best, for example Redux has as a principle that you should only have one Store, while in Flux this is not the case.
It is important to understand that none of this is an obligation or rule to handle the FrontEnd architecture, Dan Abramov who is a React guru, a Facebook collaborator for the creation of React JS and a creator of Redux, told us that Facebook itself does not use Redux , despite the fact that it is the bookstore that the community adopted for a long time as a standard.
Actually Facebook doesn’t use Redux “at scale”, it uses Flux :-)
Well, since we understand the principles of this architecture, we will see its implementation, with React useContext
and React useReducer
.
Hook - useContext
React's component tree allows data from parent components to child components to be shared through props
, this is functional for applications that are not large or that are not designed to implement a FrontEnd architecture as structured as the one we are going to build. To use Flux we will use a Hook that gives us react named useContext
, this is the one that will allow us to share data between all the components without needing to send anything through props.
How does it work? When creating a context
this will give us a Provider
component in this component we will add the data that we want to share, we must make those who need to access the data be child components of this Provider
so that later when using useContext
we can access the data.
The responsibility of this Hook in the Flux architecture is to share both the dispatcher
and the current state
of the application.
Hook - useReducer
When initializing this Hook, it will return a state
and a dispatcher
to us, these two are the key during the construction of our architecture. useReducer
receives as a parameter a function commonly known as reducer
and additionally receives the initial state.
It would look something like this:
const initialState = { ... };function reducer(state, action) { ... };const [state, dispatch] = useReducer(reducer, initialState);
The reducer
function will receive two parameters state
- action
and the objective of this function will be to return the new state of the application, generally it contains a switch
to modify the state according to the action sent.
The dispatcher
is in charge of calling the reducer
function. For this reason, every time we use dispatcher
, we must send the corresponding action to it, later we will see the example in code.
Let's go to Code 🧑💻
To exemplify this article we will create a registration form, and we will have a summary view of all registered people.
Initial Config
Let's start the project with Create-React-App:
yarn create react-app flux-react-hooks --template typescript
We would have the following project in the basic template.
Now we are going to add ReactRouter to handle a couple of routes in the application.
yarn add react-router-domyarn add -D @types/react-router-dom
We are going to start structuring the project through folders so that it is much more understandable and scalable.
src
app
App.tsx
components
state
styles
App.css
index.css
index.tsx
As we see in this structure we will have to update some files so that everything continues to work mainly index.tsx
// src/index.tsximport React from 'react';import ReactDOM from 'react-dom';import './styles/index.css';import App from './app/App';ReactDOM.render(<React.StrictMode><App /></React.StrictMode>,document.getElementById('root'));
We will create the main Home
components where we will have the list of registered people, AddPerson
which will be a view where we will register people. At the moment we will only create these components to be able to put them in the project routes:
// src/components/Home/index.tsximport { ReactElement } from 'react';export const Home = (): ReactElement => {return <div>Home</div>;};
// src/components/AddPerson/index.tsximport { ReactElement } from 'react';export const AddPerson = (): ReactElement => {return <div>Add Person</div>;};
Now we will create the file to handle the project paths:
// src/app/Routes.tsximport { ReactElement } from 'react';import { Switch, Route } from 'react-router-dom';import { Home } from '../components/Home';import { AddPerson } from '../components/AddPerson';export const Routes = (): ReactElement => {return (<Switch><Route exact path='/'><Home /></Route><Route exact path='/add-person'><AddPerson /></Route></Switch>);};
We will now update the App file to be able to access these routes.
// src/app/App.tsximport { BrowserRouter } from 'react-router-dom';import { Routes } from './Routes';import { Layout } from '../components/Layout';import '../styles/App.css';export default function App() {return (<BrowserRouter><Layout><Routes /></Layout></BrowserRouter>);}
In this component I added a Layout this all it has is a Navbar and a Footer that we will share in all the routes, it is really one more file to give the site aesthetics, by the way modify the styles of the site.
And finally within the general topics of the project, I will create a file to have the data types of the project.
// src/app/types.tsexport interface Person {id: number;name: string;lastName: string;jobTitle: string;}
Until now the site would look like this:
Flux Architecture to Code
We will now enter to configure if everything related to state management based on Flux, when using Flux we could have more than one store, this is why within the state
folder we will have subfolders in relation to each type of store that we will have.
We will start creating the types of actions that we can have:
// src/state/RegisteredPeople/types.tsexport enum ActionType {ADD_PERSON = 'ADD_PERSON',REMOVE_PERSON = 'REMOVE_PERSON',}
And now that we have the types of actions we will create the action as such, if we remember the theory it told us that an action had its type and additional data that it carries.
// src/state/RegisteredPeople/actions.tsimport { Person } from '../../app/types';import { ActionType } from './types';export type Action =| { type: ActionType.ADD_PERSON; payload: Person }| { type: ActionType.REMOVE_PERSON; payload: number };
We see here that each type of action has its data, in this case by convention we will call it as payload
, it should be noted that some actions may not have a payload. Now this structure when using typescript will allow the actions to be validated.
Why don't I use something like: type Actions = 'ADD_PERSON' | 'REMOVE_PERSON'? The answer is simple, if I don't use an Enum, the actions would not be tied to a specific type of payload and this could lead to confusion when calling these actions in the future.
We will create the Dispatcher
type in it, what is needed is to receive an argument of the Action
type, additionally I will take advantage of it to create the State type.
// src/state/RegisteredPeople/types.tsimport { Person } from '../../app/types';import { Action } from './actions';export enum ActionType {ADD_PERSON = 'ADD_PERSON',REMOVE_PERSON = 'REMOVE_PERSON',}export type Dispatch = (action: Action) => void;export interface State {Persons: Person[];}
Now that we have these types defined it's time to put them to work.
First we build the function reducer
, which, as we saw, will be in charge of modifying the state
and returning a new one, as we see in the file, a switch is created to handle each type of action.
// src/state/RegisteredPeople/reducer.tsimport { ActionType, State } from './types';import { Action } from './actions';export const Reducer = (state: State, action: Action) => {switch (action.type) {case ActionType.ADD_PERSON:return { People: [...state.People, action.payload] };case ActionType.REMOVE_PERSON:return {People: state.People.filter(person => person.id !== action.payload),};default:return state;}};
Now we will create the main file where we will have to create the context and use the hook useReducer
. I will explain little by little what the file contains.
We will create the initial state of the project:
const initialState: State = { People: [] };
When creating the context we must assign the types that it will have and initialize it, createContext
is provided by the React library, remember that this is the one who will give us the provider
component to share the data.
interface ContextType {state: State;dispatch: Dispatch;}const RegisteredPeopleContext = createContext<ContextType | undefined>(undefined);
Now we will create the component to export and in this we will use the provider
that createContext
gives us:
interface Props {children: ReactNode;}export const RegisteredPeopleProvider = ({ children }: Props): ReactElement => {const [state, dispatch] = useReducer(Reducer, initialState);return (<RegisteredPeopleContext.Providervalue={{state: state,dispatch: dispatch,}}>{children}</RegisteredPeopleContext.Provider>);};
We see that within this I am initializing the useReducer
and that I send within the Provider
component a prop called value, this will be accessed later from the useContext
hook, this could be done from any component that is a child of the provider.
This is how the complete file would be.
// src/state/RegisteredPeople/context.tsximport { useReducer, createContext, ReactNode, ReactElement } from 'react';import { Reducer } from './reducer';import { State, Dispatch } from './types';// Initialize the stateconst initialState: State = { People: [] };// The context that we will create must have its type.interface ContextType {state: State;dispatch: Dispatch;}// Create the contextconst RegisteredPeopleContext = createContext<ContextType | undefined>(undefined);interface Props {children: ReactNode;}// Create the new Providerexport const RegisteredPeopleProvider = ({ children }: Props): ReactElement => {const [state, dispatch] = useReducer(Reducer, initialState);return (<RegisteredPeopleContext.Provider// Send state data// Send the dispatchervalue={{state: state,dispatch: dispatch,}}>{children}</RegisteredPeopleContext.Provider>);};
To expose the data of this state to the rest of the application we will use an index file, in this we will only expose that we are interested in the rest of the app knowing, at the moment only the Provider
that we create in the context
.
// src/state/RegisteredPeople/index.tsexport { RegisteredPeopleProvider } from './context';
We will use the Provider
in the main component of the application to give access to all the child components of this context
.
// src/app/App.tsximport { BrowserRouter } from 'react-router-dom';import { Routes } from './Routes';import { Layout } from '../components/Layout';import { RegisteredPeopleProvider } from '../state/RegisteredPeople';import '../styles/App.css';export default function App() {return (<BrowserRouter><RegisteredPeopleProvider><Layout><Routes /></Layout></RegisteredPeopleProvider></BrowserRouter>);}
After this configuration from any component we could use useContext
and access this data, useContext
would have to receive the created context
in its parameters, it means that in each component when using this hook we must import it and also import the context
, this as we can see will be a repetitive action that we can group in a custom hook, so that in this way it is easier to access.
Example without custom hook, for this we must export the context from the index file:
// src/state/RegisteredPeople/index.tsexport { RegisteredPeopleProvider, RegisteredPeopleContext } from './context';
Then we use it, also importing the useContext
from React.
// src/components/Home/index.tsximport { useContext, ReactElement } from 'react';import { RegisteredPeopleContext } from '../../state/RegisteredPeople';export const Home = (): ReactElement => {const context = useContext(RegisteredPeopleContext);console.log(context?.state);return <div>Home</div>;};
Example with custom hook, first we create it at the end of the same context file:
// src/state/RegisteredPeople/context.tsxexport function useRegisteredPeople() {const context = useContext(RegisteredPeopleContext);if (context === undefined) {throw new Error('useRegisteredPeople must be used within a RegisteredPeopleContext Provider');}return context;}
We export it - index.ts
// src/state/RegisteredPeople/index.tsexport { RegisteredPeopleProvider, useRegisteredPeople } from './context';
And finally, this is how we would use it:
// src/components/Home/index.tsximport { ReactElement } from 'react';import { useRegisteredPeople } from '../../state/RegisteredPeople';export const Home = (): ReactElement => {const context = useRegisteredPeople();console.log(context?.state);return <div>Home</div>;};
As you can see, the implementation with the custom hook is much more understandable and simple to use.
Now to use the architecture created we are going to code the main components of this example
Home
: It will use another component named PersonCard
AddPerson
: It will have a form inside it.
// src/components/Home/PersonCard.tsximport { ReactElement } from 'react';import { Person } from '../../app/types';import { useRegisteredPeople, ActionType } from '../../state/RegisteredPeople';interface Props {person: Person;}export default function PersonCard({ person }: Props): ReactElement {const { dispatch } = useRegisteredPeople();const handleDeletePerson = () => {dispatch({ type: ActionType.REMOVE_PERSON, payload: person.id });};return (<div className='PersonCard'><imgsrc={`https://ui-avatars.com/api/?background=random&size=128&rounded=true&name=${person.name}+${person.lastName}`}alt={`Profile ${person.name} ${person.lastName}`}className='PersonCard-Img'/><h3 className='PersonCard-Name'>{person.name} {person.lastName}</h3><p className='PersonCard-Job'>{person.jobTitle}</p><button className='PersonCard-Button' onClick={handleDeletePerson}>Delete ✖</button></div>);}
// src/componentes/Home/index.tsximport { ReactElement } from 'react';import { useHistory } from 'react-router-dom';import { useRegisteredPeople } from '../../state/RegisteredPeople';import PersonCard from './PersonCard';import '../../styles/Home.css';export const Home = (): ReactElement => {const { state } = useRegisteredPeople();const history = useHistory();return (<section className='Home'><div className='Add-Button'><button onClick={() => history.push('/add-person')}>Nuevo Registro</button></div><div className='PeopleList'>{state.People.map(person => (<PersonCard key={person.id} person={person} />))}</div></section>);};
With this we would be ready to display data, now we must add it.
For the AddPerson
component I will use the dispatcher
to add new people to the state
.
// src/componentes/AddPerson/index.tsximport { useState, ReactElement } from 'react';import { useHistory } from 'react-router-dom';import { FormEventHandler, ChangeEventHandler } from 'react';import { ActionType, useRegisteredPeople } from '../../state/RegisteredPeople';import '../../styles/AddPerson.css';interface NewPersonForm {name: string;lastName: string;jobTitle: string;}const initialForm = {name: '',lastName: '',jobTitle: '',};export const AddPerson = (): ReactElement => {const [form, setForm] = useState<NewPersonForm>(initialForm);const { state, dispatch } = useRegisteredPeople();const history = useHistory();const handleChange: ChangeEventHandler<HTMLInputElement> = event => {const { name, value } = event.target;setForm({ ...form, [name]: value });};const handleSubmit: FormEventHandler<HTMLFormElement> = event => {event.preventDefault();const lastPerson = state.People[state.People.length - 1];const id = lastPerson ? lastPerson.id + 1 : 1;dispatch({type: ActionType.ADD_PERSON,payload: {...form,id,},});history.push('/');};return (<section className='AddPerson'><h1>Nuevo Registro</h1><form className='AddPerson-Form' onSubmit={handleSubmit}><div className='AddPerson-Form-Group'><label htmlFor='name'>Name</label><inputvalue={form?.name}onChange={handleChange}type='text'name='name'id='name'/></div><div className='AddPerson-Form-Group'><label htmlFor='lastName'>Last Name</label><inputvalue={form?.lastName}onChange={handleChange}type='text'name='lastName'id='lastName'/></div><div className='AddPerson-Form-Group'><label htmlFor='jobTitle'>Job Title</label><inputvalue={form?.jobTitle}onChange={handleChange}type='text'name='jobTitle'id='jobTitle'/></div><button className='AddPerson-form-button' type='submit'>Registrar</button></form></section>);};
This is how the form would look visually:
And so the Home
component would look like when adding the data:
With this we would be finalizing the code, remember that at the beginning of this article is the link to github.
Conclution
As we can see, it is an architecture that allows us to scale applications in a simple way, we do not need any library to do this and it teaches us that it is important to understand what is behind each library, understanding it will make it much easier for us to take advantage of these. and not have a dependency.
There are more optimizations to the written code that can be done, I recommend the following Blog where you can investigate more about good practices when writing code in React.