- Clone the repository
npm i && npm start
- Webpack should be running on
localhost:3000
Note: The following instructions are condensed and not very explanative. I highly encourage readers to visit the official Redux docs, which are excellent and contain links to two free video series by the Redux creator, Dan Abramov.
Refer to the completed source code on the redux
branch when stuck.
-
Install packages:
npm i redux npm i react-redux npm i redux-thunk npm i --save-dev redux-devtools
-
Create a
store
directory with areducers.js
file and build your todos reducer, which has to be a pure function with two params:state
andaction
. The first parameter should have a default object that is kind of like your state "database schema"; at least showing the keys involved in the app. For us, it's likely just going to have 1 key:const DEFAULT_STATE = [];
The todos reducer should contain a
switch
statement onaction.type
that matches the three possible actions in our app:- ADD_TODO
- REMOVE_TODO
- TOGGLE_TODO
To remain pure/idempotent, the function needs to return a new state each time, which means we need methods that return a new
todos
array. -
In the
store
directory, create a new directoryactions
with two new files:actionCreators.js
andconstants.js
. Your action creators should be named camelCase according to the action types they generate. Also,constants
should just have a list of action types named according to their strings. -
Refactor the reducer to import types from constants instead of strings.
-
Change
index.js
to have a provider and store. The store is generated with the ReduxcreateStore
method, and it accepts your root reducer as the first argument along with middleware Apply the thunk middleware and the dev tools extension like so:import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import { Provider } from 'react-redux'; import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import registerServiceWorker from './registerServiceWorker'; import App from './components/App'; import todoReducer from './reducers/todo'; const store = createStore( todoReducer, compose(applyMiddleware(thunk), window.devToolsExtension ? window.devToolsExtension() : f => f) ); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); registerServiceWorker();
-
Create a
containers
directory which will contain Redux wrappers around React components. In our case, we'll just wantToDoList
to be a smart (Redux-aware) container, so we'll createToDoListContainer.js
. -
The ToDoListContainer is going to import the ToDoList react component as well as action creators. Two functions are to be defined:
mapStateToProps
andmapDispatchToProps
; the first one maps Redux state to React props while the second one maps Redux action creators to React props. These functions are passed as the first two arguments, respectively, to thereact-redux
package'sconnect
function. Theconnect
function is then composed against our React component like so:const ToDoListContainer = connect(mapStateToProps, mapDispatchToProps)(ToDoList);
This connected component becomes the default export for our container.
-
In our
components/App.jsx
, instead of importing and renderingToDoList
, we now have replace it withToDoListContainer
. -
The last step is to refactor the original
ToDoList
component. The state should be modified to only concern itself with presentational components; the business logic (in this case,todos array
) is handled by Redux. So state can be simplified like so:this.state = { loading: true, newTodo: '' };
Also, all of the click handlers should now dispatch Redux action creators via props rather than making direct API or state changes:
toggleTodo(id) { this.props.toggleTodo(id); }
Lastly, we need to add an additional lifecycle method
componentWillRecieveProps
to update state when a new todo has been submitted. We do this by only clearing out our input value (newTodo
in state) if the incomingtodos
array has a greater length than previously (note that this is kind of a cheap shortcut instead of doing a more expensive deep array comparison, but it should be okay for this example):componentWillReceiveProps(nextProps) { // if we've added a todo, reset the newTodo form if (this.props.todos.length < nextProps.todos.length) this.setState({ newTodo: '' }); }
This should mostly conclude the conversion to Redux; refer to the redux
branch for completed source code which is working. There are some additional things I did not cover (using a thunk for the FETCH_TODOS_REQUEST
action creator for instance), but this is simply a rapid intro to converting an app to Redux and there is much further research necessary to lock these concepts down.
Lastly, here is an example flow comparison of the two versions of the app:
React Version
- ToDoList fetches todos from the API directly and adds them to state
- ToDoList renders the list from state
- New todos are input by the child form component
AddToDo
that get added to theToDoList
state.
Redux Version
- ToDoList dispatches an action (a thunk) to fetch todos from the API and dispatch a success or fail action.
- The reducer puts the recently-acquired todos into Redux state.
- ToDoList re-renders due to its subscription to
mapStateToProps
fortodos
. - New todos are input by the child form component
AddToDo
that dispatchADD_TODO
actions which go through the reducer and add them to Redux state. - Redux state changes are picked up by
componentWillReceiveProps
, which then clears out the input box onToDoList
component.
You can see the Redux flow is much more complex, but it's also much more trackable and thus, reproducible. There are still tradeoffs, and in the example of this simple app, it seems much easier to go straight React rather than React + Redux.