After covering the basics of redux in my other post, Introduction to Redux, the next logical step is to illustrate how react components connect to the redux store.
The key package that makes it possible for these two technologies to work together is the react-redux
.
To easily get this project started, you should have create-react-app
package globally installed, if you do not have that then quickly install it like so:
npm install -g create-react-app
Then to create a fully functional starter app just use the above package like so:
create-react-app name-your-app-what-ever-you-like
The benefits of starting this way is that all the boilerplate configuration - that have nothing to do with the actual react app but simply how it's run is already configured for us.
You would then start the app with npm start
which will run your app in port 3000
.
Having said all that if you never have worked with react, then this is not the right tutorial for you. Also, if you haven't played with redux before then I highly recommend going through my other tutorial on redux first.
Let's get started
The best way I learn new things is by using fewer files as possible. With that in mind, we are just going to use the src/index.js
file. It's not the recommended way of working, for the obvious reason that modularity, braking the code in different files is one of the ways to keep the code clean and maintainable. But for our purpose this is better.
At the end of the tutorial I'll have links to the code we use in the index.js
, plus I'll touch on how we can refactor the code to make it more modular.
Packages we'll need installed
Everything we need to work with react was installed by create-react-app
command, all we need to install in addition are the following packages redux
and react-redux
. We can do so by running
npm i redux react-redux
Or if you use yarn
yarn redux react-redux
Whilst redux
module doesn't need to be explained, react-redux
is a module which makes the connection between react and redux.
As mentioned above, open src/index.js
, delete what's on there and let's get started by importing our packages.
import ReactDOM from 'react-dom';
import React, { Component } from 'react';
import { Provider, connect } from 'react-redux';
import { createStore, compose } from 'redux';
The first two were already installed by the app creator we ran above.
Working with redux
Reducers are functions that listen to the actions dispatched to redux and returns an immutable version of the store state.
When the app loads for the first time, the reducer is fired but there is no action, so it returns the initial state. We want the initial state to be an empty array (as specified as part of state
argument, line one). Then if an action with the type of CREATE_TODO
is fired, the reducer returns a new state adding the results of the action into the array.
const todos = (state = [], action) => {
switch (action.type) {
case 'CREATE_TODO':
return state.concat([action.text])
default:
return state;
}
};
Next, let's create the store by using the createStore
method provided by redux
. It accepts three possible arguments: a reducer, the preloadedState, and enhancers(these are explained in redux documentation), only the first argument is a must.
const store = createStore(
todos,
undefined,
compose(
window.devToolsExtension ? window.devToolsExtension() : f => f
)
)
Since we have already specified the state in the reducer, we set the second argument to undefined
, however, if you set it to an empty array or to, say, ['item one']
it would simply mean the reducer would use it as the initial state (great for preloading data that you might retrieve from an API).
The enhancer we used (third argument) simply allows our app to interact with redux browser extension (if you don't have in installed you can get more information here). It has no effect on the actual app, it's simply a tool to help you as a developer.
Using the redux store in in react components
A very basic react setup would look like this:
class App extends Component {
render() {
return (
<h1>Hello world</h1>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root'));
A react component which returns Hello World
is rendered on the page, inside an element with the id of root
.
As it stands, our react component isn't aware of the redux store.
To make the connection we have to utilise the react-redux
module which gives us two additional components, Provider
and connect()
, both of which allow react to interact with redux.
As the names suggests, Provider
provides the store to our entire react application and connect
enables each react component to connect to the provided store.
Remember, we have already imported these two methods in our src/index.js
file.
import { Provider, connect } from 'react-redux';
From the react-redux documentation we learn that:
<Provider store>
Makes the Redux store available to theconnect()
calls in the component hierarchy
So let's do that. Let's make the Redux store available to connect()
and in turn give our react component access to the store.
class App extends Component {
//...
}
const MyApp = connect( state => ({
todos: state
}), { createTodo })(App);
ReactDOM.render(
<Provider store={store}>
<MyApp />
</Provider>,
document.getElementById('root'));
MyApp
is our App
component with the added benefit of having the store and actions injected in its state.
Again, Provider
gets the store and passes it to connect()
and connect()
passes it to the react component.
What is connect()
really doing?
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
[
connect()
is] A higher-order React component class that passes state and action creators into your component derived from the supplied arguments. - From documentation
const MyApp = connect( state => ({
todos: state
}), { createTodo })(App);
First argument, mapStateToProps
, gets the state (which is made available by the Provider
) assigns a variable name todos
and passes it into the props of our component.
The next argument, [mapDispatchToProps]
, passes our createTodo
action to the component props as well. The createTodo
is a function that returns the object that reducers listen for.
const createTodo = (text)=>{
return {
type: 'CREATE_TODO',
text
}
}
(Again, we covered those in the previous Introduction to Redux tutorial)
Working with the react component
Now we have access to the redux store state from the react App
component. Let's finally interact with the store from there.
class App extends Component {
_handleChange = e => {
e.preventDefault()
let item = e.target.querySelector('input').value;
this.props.createTodo(item);
}
render() {
return (
<div>
<form onSubmit={this._handleChange}>
<input type="text" name="listItem" />
<button type="submit">button</button>
</form>
<br />
{this.props.todos.map((text, id) => (
<div key={id}>
{text}
</div>
)) }
</div>
);
}
}
Focus on the render()
method first. We return a form. Upon submit _handleChange
method is triggered. From there the createTodo
action is dispatched.
Further down, we loop through the todos
array (which we constructed in connect()
component) and render them on the page.
Note: whenever we loop through a list to render the value, react requires us to provide a unique key, otherwise we get a warning of: Warning: Each child in an array or iterator should have a unique "key" prop.
The documentation explains why react requires unique keys to be passed to each element:
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity
And that's it.
We've created a simple todo list where we can add items to redux store and display them back, from the redux store to the react component.
Between this tutorial and the Introduction to Redux you could build upon this to add other functionalities such as delete, archive and edit. All the heavy lifting for this extra functionality would go into redux reducers and actions. In the react App
component only few buttons to trigger the extra actions would need to be added.
Going modular
All the code we covered so far goes into one file, the src/index.js
. I made the file available here
In a proper application this code would be modularised into separate files. Here is one way to do that. The code is the same, we simply take advantage of import/export features of ES6:
In src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import MyApp from './App';
ReactDOM.render(
<Provider store={store}>
<MyApp />
</Provider>,
document.getElementById('root'));
In src/App.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createTodo } from './actions/todos';
class App extends Component {
// exact same code
}
export default connect( state => ({
todos: state.todos
}), { createTodo })(App);
In src/actions/todos.js
This is where all the actions such as deleteTodo
would go, but we only had one:
export function createTodo(text){
return {
type: 'CREATE_TODO',
text
}
}
In src/store.js
import { combineReducers } from "redux";
import { createStore, compose } from 'redux';
import todos from './reducers';
const rootReducer = combineReducers({
todos
})
export default createStore(
rootReducer,
undefined,
compose(
window.devToolsExtension ? window.devToolsExtension() : f => f
)
)
In src/reducers/index.js
If we had more than one reducer, we would utilise the combineReducers
module as we did in the Introduction to Redux tutorial, but as it stands now, we just transfer our one reducer here, like so:
export default (state = [], action) => {
switch (action.type) {
case 'CREATE_TODO':
return state.concat([action.text])
default:
return state;
}
};
Thanks for reading!
Top comments (1)
Thanks for this comprehensive tutorial!