By Kevin Hou
9 minute read
Redux has been making a strong appearance recently in the world of web development. In a nutshell, it is essentially a way of keeping and consolidating props outside of the React components. The goal is to eliminate the need for local states and use the React components for purely rendering and small minor helper functions. The creators of Redux set out to tackle the challenge many developers faced, myself included, of passing properties between many components.
I've been working with React since it's birth for about 3 years now and I've been a huge fan ever since. I started using Redux this past summer at Moat (Now Oracle as of August 1st) and it's been a great addition to my skillset and productivity because of the ways in which it improves large-scale React web apps.
In this post, I will cover the basics of React/Redux as well as its benefits. Redux works by using React components, containers, actions, and reducers. Reducers receive action dispatches and update the Redux state. All Redux state changes must be routed through the reducer. Additionally, all actions must be connected to components through containers. Component can only update the local state by triggering actions. In short, reducers sit on top of the state, actions triggers reducers, containers connect components with states as well as actions, and components display client-facing components.
client/src/
+-- Actions
| +-- Section1Actions.js
| +-- Section2Actions.js
+-- Components
| +-- Section1
| | +-- Components1.jsx
| | +-- Components2.jsx
| +-- Section2
| | +-- Components3.jsx
| | +-- Components4.jsx
+-- Constants
| +-- MainActionTypes.js
| +-- OtherConstants.js
+-- Containers
| +-- EntryContainer.js
| +-- Section1Container.js
| +-- Section2Container.js
+-- Reducers
| +-- Section1Reducer.js
| +-- Section2Reducer.js
| +-- index.js
+-- Styles
| +-- main.scss
| +-- Section1Styles.scss
| +-- Section2Styles.scss
+-- ConfigureStore.js.
+-- index.js
react
react-dom
react-redux
redux
redux-logger
redux-thunk
1// client/src/index.js 2 3import React from 'react'; 4import { render } from 'react-dom'; 5import { Provider } from 'react-redux'; 6 7// This imports a redux store that we will get into next 8import store from './ConfigureStore.js'; 9 10// I've found it helpful to use a generic entry container that houses subsequent containers 11import Entry from './Containers/EntryContainer'; 12 13// Can call actions on render which can be helpful for fetch requests 14import { someAction } from './Actions/MainActions'; 15 16// Import styleguide (optional) that doesn't correspond to a specific component 17import './Styles/main.scss'; // Get styleguide 18 19store.dispatch(fetchRepos()); // Fetch the fe-component repos 20 21render( 22 // Must wrap with the provider and store 23 <Provider store={store}> 24 <Entry /> 25 </Provider>, 26 27 // The target in the DOM you are rendering to 28 // This will be in the client/dist/index.html 29 document.getElementById('main') 30); 31
1// client/src/ConfigureStore.js 2 3import { createStore, applyMiddleware } from 'redux'; 4import thunkMiddleware from 'redux-thunk'; 5import createLogger from 'redux-logger'; // For debugging in the console 6 7import mainReducer from './Reducers'; // Import the reducer (client/src/Reducers/index.js) 8 9// Thunk middleware is used to allow functions to be passed as actions 10const middlewares = [thunkMiddleware]; 11 12// If debug mode is on, log state changes and actions to the console 13const debuggerMode = false; // Can also be determined by process.ENV variables 14if (debuggerMode) { 15 const loggerMiddleware = createLogger(); 16 middlewares.push(loggerMiddleware); // Add middleware 17} 18 19// Creates store that handles the complete state tree of app 20// This is exported and used by the provider 21export default createStore(mainReducer, applyMiddleware(...middlewares)); 22
Components are the purely frontend, client-side rendering piece of the puzzle. They should have limited logic and mainly act as pure functions. They are connected to containers which feed them props, but for now we will simply be creating a component with property requirements.
Pure Functional Component (Just Rendering)
1// client/src/Components/<folder-name>/<component-name>.jsx
2
3import React from 'react';
4import PropTypes from 'prop-types'; // For enforcing prop types
5
6import '../../Styles/<component-name>.scss'; // Import the corresponding stylesheet
7
8const ComponentName = (props) => (
9 <div>
10 {props.message}
11 </div>
12);
13
14// Declare which props are what type and if they are optional
15ComponentName.propTypes = {
16 message: PropTypes.string.isRequired,
17 someObject: PropTypes.shape({
18 name: PropTypes.string.isRequired,
19 age: PropTypes.number,
20 numbers: PropTypes.arrayOf(PropTypes.number).isRequired,
21 }), // Not required
22};
23
24export default ComponentName;
25
More Logic-Heavy Component
1// client/src/Components/<folder-name>/<component-name>.jsx
2import React, { Component } from 'react'; // Must import the Component class type
3
4import '../../Styles/<component-name>.scss'; // Import sass file for this component
5
6class Loading extends Component {
7 constructor(props) { // Can now use other functions
8 super(props);
9 ...
10 }
11
12 randomFunction(input) {
13 ...
14 }
15
16 render() {
17 return (
18 <div>
19 {this.props.message}
20 </div>
21 )
22 }
23}
24
25ComponentName.propTypes = {
26 ...
27};
28
29export default ComponentName;
30
All actions must dispatch an object that dictates to a reducer what it should do. The way the reducer registers what type of logic to carry out is by a unique action.type. These are stored in the Constants/MainActionTypes.js file. It looks something like this:
1// client/src/Constants/MainActionTypes.js 2 3// Loading Actions 4export const REQUEST_SENT = 'REQUEST_SENT'; 5export const REQUEST_SUCCESS = 'REQUEST_SUCCESS'; 6 7// API Actions 8... 9
Reducers respond to actions when they dispatch a return object. These objects are picked up by the reducers and some logic is carried out that can update the state. Reducer functionality should be kept at a minimum and it is very important that they remain pure-functions — that is, they only rely on input, no other information, for calculating an output. A reducer looks like this:
1// client/src/Reducers/SomeReducer.js
2
3// This gives you access to the action types we created in step 4
4import * as types from '../Constants/MainActionTypes';
5
6const initialState = {
7 someValue: true, // Default initial state for this variable
8};
9
10const someReducer = (state = initialState, action) => {
11 // The app can decide what to do based on the action type
12 // The return object for this function will be the new state
13 switch (action.type) {
14 case types.SOME_ACTION_TYPE:
15 return {
16 ...state,
17 someValue: false,
18 };
19 default: // Default, no state change
20 return state;
21 };
22 return state;
23};
24
25export default someReducer;
26
The reducers are then consolidated into one file in the Reducers/index.js file:
1// client/src/Reducers/index.js
2
3import { combineReducers } from 'redux';
4
5// Import all the reducers
6import someReducer from './someReducer';
7...
8import anotherReducer from './anotherReducer';
9
10// Export for use in the entry index.js file
11export default combineReducers({
12 // 'name' refers to the name of the reducer you will use to access the variables associated with it
13 name: someReducer,
14 ...
15 another: anotherReducer,
16});
17
Actions are like any function except that the only thing that really matters is their return object. Actions are dispatched (thanks to redux-thunk) and their return objects are what the reducers receive and interpret; therefore, it is imperitive that the action return has type value associated with it. We will set them using the MainActionTypes.js constants we wrote earlier so that there are no typos. Actions are generally referenced in the container and attached to a component as a prop as we'll see in step 7.
Standard action:
1// client/src/Actions/mainActions.js
2
3import 'whatwg-fetch'; // Fetch requests
4import * as types from '../Constants/MainActionTypes';
5
6export const updatingVariables = (val) => ({
7 type: types.SOME_ACTION,
8 name: val,
9});
10
Sample async fetch-request:
1// client/src/Actions/mainActions.js
2
3import 'whatwg-fetch'; // Fetch requests
4import * as types from '../Constants/MainActionTypes';
5
6export const fetchJSON = () => {
7 const url = '...';
8
9 // Returning a promise allows you to use the 'dispatch' function in the child scope
10 return (dispatch) => {
11 dispatch(requestSent(url)); // Actions can dispatch other actions
12
13 // Return the contents of the fetch promise
14 return fetch(url, { // See watwg-fetch for docs
15 credentials: 'same-origin', // If same origin
16 }).then(response => response.json()) // Parse response
17 .then(json => {
18 console.log('Received data');
19 dispatch(receivedData(json));
20 return json; // Return it as a promise — will be the result of the original action
21 // Example:
22 // this.props.fetchJSON().then((response) { ... }).catch((error) { ...});
23 })
24 .catch(error => { // Catch any errors
25 dispatch(actionError(error));
26 });
27 };
28};
29
Containers put steps 3-5 together and lets you package the component, reducer, and constants all together. Containers connect the actions, redux state values, and reducers to the components as props. When you want to render the contents of the container, you include the <ContainerName /> as opposed to its child <ComponentName />.
1// client/src/Containers/someContainer.js
2
3import { connect } from 'react-redux';
4
5// Get the component
6import SomeComponent from '../Components/SomeComponent';
7
8// Import actions you'd like to make usable to a component
9import { someAction } from '../Actions/MainActions';
10
11// Map the redux states to props
12const mapStateToProps = (state) => ({
13 // 'name' is the name of the reducer you specified in step 5 in the file:
14 // client/src/Reducers/index.js
15 someValue: state.name.someValue,
16});
17
18// Map actions to props
19const mapDispatchToProps = (dispatch) => ({
20 viewComponent: (input) => (
21 dispatch(someAction(input))
22 ),
23});
24
25// Connects your component to the store using the previously defined functions
26export default connect(
27 mapStateToProps, // Add the states
28 mapDispatchToProps // Add the actions
29)(SomeComponent); // Connect them to the component
30
To use the container, simply include it like so:
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3
4import SomeContainer from '../Containers/SomeContainer'; // Get container
5
6class Entry extends Component {
7 constructor(props) {
8 super(props);
9 ...
10 }
11
12 render() {
13 return (
14 <div>
15 <SomeContainer />
16 </div>
17 )
18 }
19}
20
21Entry.propTypes = {
22 ...
23};
24
25export default Entry;
26
27
If you put all those pieces together you should end up with your first, boilerplate React/Redux application! It's a really useful tool and a great addition to any stack — especially those that already use React. I will be working with this stack very shortly on a personal project as well as some business projects so I'll be sure to keep you all updated on what I'll continue to learn!