Hunter Johnson
Posted on March 3, 2023
As developers, we turn to design principles to help us solve problems. These serve as a kind of template that assists in quickly creating the structure of our application so we can get an MVP off the ground.
We use design patterns in React to save time by applying solutions that have been proven to work on common problems. There is no need to reinvent the wheel and refactor later because you are already using a design that works.
In this article, we’ll talk about common architectures that you can choose from to craft React your application.
Today, we'll cover:
Flux Architecture
One of the more common design patterns is called the MVC design pattern, which stands for model, view, controller. This pattern organizes your code into three boxes:
- The raw data that will be the model of your application
- The logic that takes that raw data and turns into functions that will control the state of the application
- The user interface where your client will interact with and view the application
As applications scale, the MVC data flow can become more of a problem because MVC applications have data that can move in multiple directions, creating two-way data flows. Code is difficult to maintain. If we want to iterate on problems quickly, this way of writing the code is not going to work.
Facebook solved this problem with the creation of the Flux Architecture -- a design pattern that only allows data to flow in one direction while working in conjunction with React to only update a web page when the state of a component changes.
Parts of the Flux Architecture
1. Action
The action is what triggers the sequence of events that will take place that will ultimately end up with the UI to be re-rendered in React. An action has a type property and the new data:
//action types
export const ADD_USER = "ADD_USER";
export const UPDATE_USER = "UPDATE_USER";
//action creators
export const addUser = (payload) => {
return ({
type: ADD_USER,
payload
})
}
export const updateUser = (payload) => {
return ({
type: UPDATE_USER, payload
})
}
The type property describes the action, and the payload is the new data that is passed. That payload could be of any type (object, string, etc.) depending upon the action creator it will be used for. In this instance, they will probably both be objects since we will probably be dealing with a user ID and a username or email address.
These action creators are the responsibility of the dispatcher when invoked.
2. Dispatcher
The dispatcher is essentially the traffic controller in this scenario. It’s a directory of callback functions that will be used to update the store. Like a police dispatcher will tell a cop car where to go for an emergency, a Flux Dispatcher tells us what an action should do.
var Dispatcher = function () {
return {
_stores: [],
register: function (store) {
this._stores.push({ store: store });
},
// checks for updates in each store
dispatch: function (action) {
if (this._stores.length > 0) {
this._stores.forEach(function (entry) {
entry.store.update(action);
});
}
}
}
};
3. Store
The store deals with our application state. In Flux, we can have multiple stores, one for each component if we wanted to. State is an object that holds key value pairs that assist in the rendering of that particular component and/or its children.
import React, { Component } from 'react';
class SampleContainerComponent extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
user: {
name: "Jane Doe",
age: 42,
numKids: 0
},
movie: {
title: "E.T.: The Extra-Terrestrial",
director: "Steven Spielberg",
music: "John Williams"
}
}
}
render() {
return (
<>
<Movie movie={this.state.movie} />
<User user={this.state.user} />
</>
);
}
}
export default SampleContainerComponent;
These components will use the state they have in common through the passing of props. This continues to promote that unidirectional data flow by passing the logic to be used to update state in the stateful class component and then render the actual props that the component will display on screen after the logic has been executed.
4. View
The view of the application does not change until state has been updated. This prevents automatic re-renderings of components that are static or don’t need to be changed. Only the components whose state has changed will be updated.
Full implementation of Flux architecture
Let's take a look at a full implementation of the Flux architecture. Here, if we add the support of AMD, CommonJS and global usage, we end up with 66 lines of code, 1.7KB plain or 795 bytes after minification JavaScript.
var Dispatcher = function () {
return {
_stores: [],
register: function (store) {
// expecting an `update` method for each store
if (!store || !store.update) {
throw new Error(
'You should provide a store that has an `update` method'
);
} else {
var consumers = [];
var change = function () {
consumers.forEach(function (consumer) {
consumer(store);
});
};
var subscribe = function (consumer, noInit) {
consumers.push(consumer);
!noInit ? consumer(store) : null;
};
this._stores.push({ store: store, change: change });
return subscribe;
}
return false;
},
dispatch: function (action) {
// check all stores for update
if (this._stores.length > 0) {
this._stores.forEach(function (entry) {
entry.store.update(action, entry.change);
});
}
}
}
};
module.exports = {
create: function () {
var dispatcher = Dispatcher();
return {
createAction: function (type) {
if (!type) {
throw new Error('Please, provide action\'s type.');
} else {
return function (payload) {
// actions are passed to dispatcher
return dispatcher.dispatch({
type: type,
payload: payload
});
}
}
},
createSubscriber: function (store) {
return dispatcher.register(store);
}
}
}
};
There are many great things about Flux Architecture and how it works with React. However, if our applications get large, or if we have child components that need to have props passed down more than four or five levels (a concept known as props drilling), it becomes more prone to bugs and less readable to a new engineer who might be getting up to speed on the code.
There are two more patterns that can be used to assist in improving on the Flux Architecture. Next, we’ll talk about Redux.
Redux Architecture
Redux is a library that acts as a state container and helps to manage your application data flow. It is similar to Flux architecture and has a lot in common with it. The Redux Architecture takes what we already know about the Flux Architecture and expands it so it is more efficient for grander, more complex applications.
The two biggest improvements that Redux offers is one store that is accessible and subscriptable to all React components, and a reducer inside that store that will determine the changes to your application's state.
Parts of the Redux Architecture
Much of the architecture to Redux is very similar to that of Flux Architecture. We will go over the portions of the architecture that differ from one another:
One Store to Hold State
When we think about the Flux Architecture and how it works, we know it has a unidirectional data flow. We tend to think of our React applications in terms of parent-child relationships. What if we had a child component that said, for instance, needed to talk to a cousin?
If using Flux Architecture, the state would be lifted up to both components' nearest common ancestor. This gets to be a bit complex when that nearest common ancestor is five generations back. This can lead to a problem called props drilling and makes your code a bit less readable. This is where Redux comes in.
Redux encapsulates our React component with a Provider
that gives us a Store
that contains most of our application’s state. Components
connect to this store and subscribe via mapStateToProps
to the state they need to use in that particular component.
CreateStore
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { taskReducer } from './reducers/taskReducer';
import './index.css';
import App from './App';
const store = createStore(taskReducer);
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));
Connect to Component via MapStateToProps
const mapStateToProps = state => {
return {
tasks: state.tasks
}
}
export default connect(mapStateToProps, { addNewTask, toggleTask })(Tasks);
The data flow essentially works the same, unidirectional, but the state management is instead at a global level as opposed to a container level where there is a dependency to pass state as props to a container’s children.
Reducer that determines how the State changes
The reducer is passed into our Store as an argument. The reducer itself takes in an action and the current state of the application. The logic inside your reducer can look however you want as long as the current state is copied and then mutated. In addition, the new state can only be computed using the arguments that are passed in.
No random values can be used, no asynchronous logic, or other methods that will cause any sort of interruption to the flow. The primary job of the reducer is to help Redux keep track of how the state has changed over time.
//Example Reducer for a ToDo List Application:
import { ADD_TASK, TOGGLE_COMPLETE } from '../actions';
const initialState = {
tasks: [
{task: 'Wash the car', complete: false},
{task: "Laundry", complete: false}
]
}
export const taskReducer = (state = initialState, action) => {
switch(action.type) {
case ADD_TASK:
return {
...state, tasks: [
...state.tasks, { task: action.payload, complete: false}
]
}
case TOGGLE_COMPLETE:
return {
...state, tasks: state.tasks.map((item, index) => {
if(action.payload === index) {
return {
...item, complete: !item.complete
}
} else {
return item;
}
})
}
default:
return state;
}
}
Final Thoughts on Redux
The beauty of Redux is that we have one source of truth where our state is held. There is a reducer (or reducers) that receive an action from a dispatcher/event handler and then take that action to tell the store what to do with it.
When the store updates, we then see a re-rendering of the component that updated in the UI. We can see how the state changes over time using the Redux DevTools.
Redux is a great tool to use if you need it. Pay careful attention to the structure of your database, how it will be used, and how the flow of your application will proceed. There are many times that Redux is used when it’s not necessarily needed. Only truly use it if you need it.
The last design pattern we will talk about is what many might consider a replacement for Redux: Context API.
Context API with React Hooks
There’s a tool now available to us to use that combines the best features of redux plus the best features of flux. It’s called the Context API and it makes state management so much easier than it once was.
In Context, we have several application-wide stores that serve to provide state at global level. It does this with a Context Provider. In this context provider are hooks that will help you create essentially a context object that will be accessible from all components in your application.
A sample usage of a Theme Context that will eventually toggle between a light and a dark theme:
import React, { useState, createContext } from 'react';
//my resources
import LightCode from '../assets/screenshotlight.png';
import DarkCode from '../assets/screenshotdark.png';
//create context to hold themes
export const ThemeContext = createContext();
export const ThemeContextProvider = ({ children }) => {
const [isLight, setIsLight] = useState(true);
//define light and dark themes...
const dark = {
backgroundPicture: `url('${DarkCode}')`,
oppositeTheme: "light",
buttonbg: "#47a5bf",
buttoncolor: "#ffe54f",
background: '#121212',
cardColor: 'darkslategrey',
primary: '#bb86fc',
primaryVariant: '#3700b3',
secondary: '#03dac6',
surface: '#121212',
error: '#cf6679',
onPrimary: '#000000',
onSecondary: '#000000',
onBackground: '#fafafa',
onSurface: '#fafafa',
onError: '#000000',
};
const light = {
backgroundPicture: `url('${LightCode}')`,
oppositeTheme: "dark",
buttonbg: "#1e39a6",
buttoncolor: "yellow",
background: '#F1EFE7',
cardColor: 'whitesmoke',
primary: '#6200ee',
primaryVariant: '#3700b3',
secondary: '#03dac6',
secondaryVariant: '#018786',
surface: "#ffffff",
error: "#b00020",
onPrimary: "#ffffff",
onSecondary: "#000000",
onBackground: "#000000",
onSurface: "#000000",
onError: "#ffffff",
};
const toggle = () => {
return isLight ? setIsLight(false) : setIsLight(true);
}
const theme = isLight ? light : dark;
return <ThemeContext.Provider value={{theme, toggle}}>{children}</ThemeContext.Provider>;
};
The value that is passed into <ThemeContext.Provider>
is how we will access our context when we pull it up in our component when we employ the useContext
hook. To utilize in your component, the syntax is this:
const { theme, toggle } = useContext(ThemeProvider);
The destructuring of the context object matches what you passed into the value prop in the <ThemeContext.Provider>
. This way you can use those properties without having to say context.toggle or context.theme.
Be sure to export this ThemeProvider
and wrap it around your entire component in index.js
so that the theme can be changed everywhere when it is toggled.
//installed packages
import "dotenv/config";
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
//my contexts
import { ThemeContextProvider } from './context/ThemeContext';
//my components
import App from './App';
import ScrollToTop from './ScrollToTop';
ReactDOM.render(
<Router>
<ThemeContextProvider>
<App />
</ThemeContextProvider>
</Router>,
document.getElementById('root')
);
You will still need to declare rules for your React components in regards to styling. For that, I would recommend your favorite CSS-in-JS package so you can access the theme when styling the colors and background colors of your components.
Final Thoughts on Context API
Context API is a great tool for getting some of the advantages of Redux without needing to have all of the boilerplate Redux requires. This makes it a popular way to design an application since React Hooks have become popular.
We are lifting state up to the context provider who passes the state down as props to our component tree so we can access it when we need it, combining the best of Redux, with the best of Flux.
What to learn next?
In this article, we took a look at three ways we can structure an application in React: Flux, Redux, and Context. All three are viable options to use depending on your individual needs. Flux is great for smaller applications where you don’t need to pass props very much.
Redux is awesome for larger scale applications that have multiple pieces of state that need to be kept track of. Context API is kind of the best of both the previous worlds. It has a global context object that is available to all of our components that’s passed to a provider as props.
The next things you'll want to learn to master React patterns are:
- Higher order components in React
- Dependency injection with React
- Styling React components
- Integration of third-party libraries
To get started on these as well as master the patterns we learned today with hands-on practice, get started with Educative's course React Deep Dive: From Beginner to Advanced. This intermediate course will teach you to make use of design patterns and think more abstractly about how you create applications in React. By the end, you'll be able to create highly complex applications with confidence!
Happy learning!
Continue reading about React on Educative
- React Router Tutorial: Adding Navigation to your React App
- Understanding Redux: Beginner's guide to modern state management
- React hooks design patterns and creating components without class
Start a discussion
What React design pattern is your favorite? Was this article helpful? Let us know in the comments below!
Posted on March 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.