React State - FLUX architecture using Context and Reducer Hooks (Typescript)

Marco Mesen Picture Profile
April 2021 | Views 208
0 Claps

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.

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 :-)

https://twitter.com/dan_abramov/status/882626668627709952

Dan Abramov - Twitter

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-dom
yarn 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.tsx
import 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.tsx
import { ReactElement } from 'react';
export const Home = (): ReactElement => {
return <div>Home</div>;
};
// src/components/AddPerson/index.tsx
import { 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.tsx
import { 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.tsx
import { 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.ts
export 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.ts
export 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.ts
import { 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.ts
import { 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.ts
import { 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.Provider
value={{
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.tsx
import { useReducer, createContext, ReactNode, ReactElement } from 'react';
import { Reducer } from './reducer';
import { State, Dispatch } from './types';
// Initialize the state
const initialState: State = { People: [] };
// The context that we will create must have its type.
interface ContextType {
state: State;
dispatch: Dispatch;
}
// Create the context
const RegisteredPeopleContext = createContext<ContextType | undefined>(
undefined
);
interface Props {
children: ReactNode;
}
// Create the new Provider
export const RegisteredPeopleProvider = ({ children }: Props): ReactElement => {
const [state, dispatch] = useReducer(Reducer, initialState);
return (
<RegisteredPeopleContext.Provider
// Send state data
// Send the dispatcher
value={{
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.ts
export { 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.tsx
import { 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.ts
export { RegisteredPeopleProvider, RegisteredPeopleContext } from './context';

Then we use it, also importing the useContext from React.

// src/components/Home/index.tsx
import { 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.tsx
export 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.ts
export { RegisteredPeopleProvider, useRegisteredPeople } from './context';

And finally, this is how we would use it:

// src/components/Home/index.tsx
import { 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.tsx
import { 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'>
<img
src={`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 &#10006;
</button>
</div>
);
}
// src/componentes/Home/index.tsx
import { 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.tsx
import { 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>
<input
value={form?.name}
onChange={handleChange}
type='text'
name='name'
id='name'
/>
</div>
<div className='AddPerson-Form-Group'>
<label htmlFor='lastName'>Last Name</label>
<input
value={form?.lastName}
onChange={handleChange}
type='text'
name='lastName'
id='lastName'
/>
</div>
<div className='AddPerson-Form-Group'>
<label htmlFor='jobTitle'>Job Title</label>
<input
value={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.

https://kentcdodds.com/

Share on