Redux
Once you start working with React in anger, there is a tipping point to be aware of where:
- the complexity of data flows piles up
- the same data is being rendered in multiple places
- the number of state changes blow out
Being able to tackle these problems in a single place is where Redux fits in.
Contents⌗
- Contents
- The Problem
- A chat with redux
- Container vs Presentation Components
- The Redux Principles
- React-Redux
- Redux Setup
- Async and APIs
- Conditional mapStateToProps
- Polish (the finer things)
- Testing
The Problem⌗
Imagine a fairly deep component hierarchy, starting with your top level App
component. Deep down the tree, there are two child components that need to access a common piece of data (e.g. customer data). How should these components access the data they require?
Option 1 lift the state⌗
This option involves placing the state (customer data) to the common ancestor in the component tree. In the worst case, this could be the top level component. It then becomes the burden of each decendent component to pass this state down as props
(known as props drilling).
In a large app, this sucks.
- keeping track of what props are being passed around
- introducing new components between an existing prop drilling path
Option 2 react context⌗
React context exposes global data and functions from a given React component. To access this published state, a downstream component imports the context and consumes.
The common ancestor parent component, could publish customer data (and functions if desired) via CustomerContext.Provider
.
The child components could consume this data via CustomerContext.Consumer
.
Option 3 Redux⌗
Provides a centralised store (like a local client side DB).
Any component can connect and query the store.
To change (mutate) data in the store is not directly possible. Instead the requesting component dispatches an action, for example Create Customer, and the data is eventually updated/created. Any components connected to this data, automatically receieve it’s freshest version.
A chat with redux⌗
This dialogue from @housecor helps drive all the moving pieces home:
- React: Hey CourseAction, someone clicked this “Save Course” button.
- Action: Thanks React! I will dispatch an action so reducers that care can update state.
- Reducer: Ah, thanks action. I see you passed me the current state and the action to perform. I’ll make a new copy of the state and return it.
- Store: Thanks for updating the state reducer. I’ll make sure that all connected components are aware.
- React-Redux: Thanks for the new data Store. I’ll determine if I should tell React about this change so that it only has to bother with updating the UI when necessary.
- React: Ooo! Shiny new data has been passed down via props from the store! I’ll update the UI to reflect this!
Container vs Presentation Components⌗
Container (aka smart, stateful or controller view) components can be thought of as the backend of the frontend. They’re concerned with behaviour and marshalling. They’re typically stateful, as they aim to keep the child components fresly rendered with the latest data. A good container component should have very minimal (ideally zero) markup.
Presentation (dumb, stateless or view) components are purely concerned with markup, and contain almost no logic. They are dumb.
Container | Presentation |
---|---|
How things work | How things look |
No markup | All markup |
Stateful | Stateless |
Aware of Redux | Unaware of Redux |
Pass data and actions down | Receive data and actions with props |
Subscribe to Redux state | Read data from props |
Dispatch Redux actions | Invoke callbacks on props |
When you notice that some components don’t use props they receive but merely forward them down…it’s a good time to introduce some container components — Dan Abramov
The Redux Principles⌗
- One immutable store. It cant be changed directly. Immutability aids debugging, supports server rendering, and opens up the ability to easily centralise state functionality such as undo and redo.
- Actions trigger changes. Components that wish to change state, do so by dispatching an action.
- State changes are handled by pure functions, known as reducers.
Conceptually this is publish subscribe. The redux flow visually:
+---------+
| ACTION |
+----+----+
|
v
+----+----+ +-----------+
| STORE +<----->+ REDUCER |
+----+----+ +-----------+
|
v
+----+----+
| REACT |
+---------+
Actions⌗
An action represents the intent via its type
property, the only mandatory field. Other data can be encoded into the action however you please.
{ type: RATE_GYM, rating: 4 }
In this example, rating could be a complex object, a boolean, or there could be several more properties in addition to rating.
Typically actions are made with a factory method known as an action creator:
rateGym(rating) {
return { type: RATE_GYM, rating: rating }
}
This is good practice as the consuming components don’t need to know about the internals of action structures. For each entity type, its common to have a CRUD (create, retrieve, update, delete) set of action creators.
The Store⌗
A store is created in the entry point of your application (i.e. as it starts up):
let store = createStore(reducer);
The store API is suprisingly simple:
store.dispatch(action)
store.subscribe(listener)
store.getState()
replaceReducer(nextReducer)
Immutability⌗
A cornerstone principle of redux, is that state is never modified (mutated), instead a completely new copy of the data is returned.
Benefits of immutable state:
- Clarity around what is responsible for state changes, everything has a clearly defined reducer. No more debugging trying to track down the piece of code responsible for mutating the state.
- Performance the job of Redux determining if a state change has occured and notifying React is greatly simplified with an immutable store. Instead of doing complex field level comparisons of each data element before and after, all Redux needs to do is a
prevStoreState !== storeState
check. - Unrivalled debugging because state is never mutated, allows for some incredible innovation in the debugging experience, such as time-travel debugging, undo/redo, skipping individual state actions, replaying the state interactions back. The redux devtools extensions opens this up.
Building complex objects up by hand each time a modification is needed, is not practical. JS does provide a number of ways to copy an object:
- Object.assign shallow copy.
Object.assign({}, state, { role: 'admin' })
assigns a new empty object based on the existingstate
object, after mixing-in the newrole
property. - The spread operator, whatever is placed on the right is shallow copied
const newState = { ...state, role: 'admin' }
- Immuatable friendly Array methods, such as
map
,filter
,reduce
,concat
andspread
. Avoidpush
,pop
andreverse
which mutate.
There is a large ecosystem of libraries available (immer, seamless-immutable, react-addons-update, Immutable.js) for working with data in an immutable compatable way, one popular option is immer:
Create the next immutable state tree by simply modifying the current tree
import produce from "immer";
const user = {
name: "Benjamin",
address: {
state: "New South Wales",
},
};
const userCopy = produce(user, (draftState) => {
draftState.address.state = "Victoria";
});
console.log(user.address.state); // New South Wales
console.log(userCopy.address.state); // Victoria
Options for enforcing immutability:
- Trust the development team, through training and coding practices.
- Warn whenever state is mutated using
redux-immutable-state-invariant
(only use in development!) - Enforce with the use of a library such as immer, immutable.js or seamless-immutable.
Reducers⌗
An action is eventually handled by a reducer. Metaphorically the reducer is the meat grinder of Redux, state goes in, state comes out.
function myReducer(state, action) {
switch (action.type) {
case "INCREMENT_COUNTER":
//state.counter++; //BAD: never mutate
//return state;
return { ...state, counter: state.counter + 1 };
default:
return state;
}
}
If and when the state is returned by a reducer, the store is updated.
Reducers must be pure. In other words a reducer must:
- Never mutate state
- Perform side effects, such as API calls or routing transitions
- Call non-pure functions (e.g.
Date.now
,Math.random()
)
Therefore for a given input, and reducer is guaranted to alway return the same output.
When a dispatch is submitted, ALL reducers are invoked. That’s why its important for the untouched state
to be returned as the default case of the switch statement.
A reducer should be independent and responsible for updates to a slice of state, however there are no hard and fast rules.
Each action can be handled by one or more reducers.
Each reducer can handle multiple actions.
Any React components that a glued up to the store are automatically updated, via a push notification using React-Redux.
React-Redux⌗
Redux isn’t exclusive to React. With React-Redux, can tie React components to state in the Redux store.
Two problems it solves:
- Attaching the app to the redux store (using a
Provider
component) - Creating container components (using the
Connect
function)
React-Redux Provider⌗
Wrapping the root App
component in a provider opens up the Redux store to every component in your application.
<Provider store={this.props.store}>
<App />
</Provider>
React-Redux Connect⌗
Before a container component is exported in the usual fashion, to connect it to the Redux store, it is wrapped with the connect
function like so:
function mapStateToProps(state, ownProps) {
return { authors: state.authors };
}
export default connect(mapStateToProps, mapDispatchToProps)(AboutPage);
mapStateToProps⌗
mapStateToProps
defines what state needs to be exposed as props
.
It returns an object that defines the data of interest. Each property defined on the object magically becomes a prop
on React component. Anytime this data changes in the store, Redux will automatically fire mapStateToProps
.
The more specific you can be in mapStateToProps
is a performance win, as it will cut down on the number of possible state change notifications Redux needs to manage.
Important everytime the component is updated, the mapStateToProps
function is called. Eeek. If there is some heavy lifting going on, such as transforming or sorting a large data structure, consider memoization (like caching for functions), where if the exact same state is presented as previously done, you can simply re-use the previously calculated results.
Memoizing libraries, called selectors, like reselect exist to make this fun.
What do selectors bring to the table?
- They are efficient. A selector is not recomputed unless one of its arguments changes.
- The can compute derived data, allowing Redux to store the minimal possible state.
- They are composable. They can be used as input to other selectors.
Here’s one in action:
const getAllCoursesSelector = (state) => state.courses;
export const getCoursesSorted = createSelector(
getAllCoursesSelector,
(courses) => {
return [...courses].sort((a, b) =>
a.title.localeCompare(b.title, "en", { sensitivity: "base" })
);
}
);
mapDispatchToProps⌗
How you expose redux actions to your components.
mapDispatchToProps
defines what actions do I want on props
. It receives dispatch
as its lone parameter, and returns the callback props you want to pass down.
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AboutPage);
Four ways to deal with mapDispatchToProps
:
Option 1: Simply ignore it (i.e. dont declare it in the call to connect
), and use the implicitly created props.dispatch
function directly in your component.
this.props.dispatch(loadProducts());
Option 2: Manually wrap calls to dispatch:
function mapDispatchToProps(dispatch) {
return {
loadProducts: () => {
dispatch(loadProducts));
},
createProduct: (product) => {
dispatch(createProduct(product));
}
}
}
To consume an action in the component becomes quite clean:
this.props.loadProducts();
Option 3: Use bindActionCreators
This ships with redux, and takes an array of actions. It will wrap each of them in a call to dispatch for you.
import * as courseActions from "../../redux/actions/courseActions";
...
...
function mapDispatchToProps(dispatch) {
return {
//this will shove all action creator functions into props
//this.props.actions.loadCourses()
actions: bindActionCreators(courseActions, dispatch),
};
}
Option 4: Return objects
Declare mapDispatchToProps
as an object, as opposed to a map. Redux connect
will automatically wrap each action creator in dispatch.
const mapDispatchToProps = {
incrementCounter,
};
Examples of mapDispatchToProps
in action:
handleSubmit = (event) => {
event.preventDefault(); //no postbacks
// console.log(this.state.course.title);
// debugger;
// option 1: implicit dispatch
// this.props.dispatch(courseActions.createCourse(this.state.course));
// option 2: simple CRUD wrappers
// this.props.createCourse(this.state.course);
// option 3: bindActionCreators
this.props.actions.createCourse(this.state.course);
};
Redux Setup⌗
Initial setup is full on:
- Create action
- Create reducer
- Create root reducer
- Configure store
- Instantiate store
- Connect component
- Pass props via connect
- Dispatch action
But once the foundation is setup, adding features becomes much easier:
- Create action type constant (
actionTypes.js
) - Create an action (new file in
/redux/actions
such asauthorActions.js
) - Create (or enhance) reducer (new file in
/redux/reducers
such asauthorReducer.js
) - Update root reducer (in
index.js
) to include new child reducer. - Connect component
- Dispatch action
Async and APIs⌗
Mock API⌗
Often state will come from another source, like the server. It’s a good idea to kick off with a mock API. Why:
- can get started immediately with getting bogged down in backend
- is resilient to backend development instabilities
- simulate and test high latency (i.e. slowness)
- testing
- seamless bind to the real API by just tweaking the imports at the top of thunks/sagas, or checking an environment variable to toggle between mock and real API.
For a simple mock API, checkout the top level tools
directory:
apiServer.js
a node mock API build using express andjson-server
, that reads in data fromdb.json
mockData.js
javascript data structures, exported in commonjs format (for node)createMockDb.js
writes mock data to a filedb.json
json-server
is COOL. Not only do you get a nice frontend over the API, it supports all HTTP verbs, so you can PUT
, POST
, DELETE
, GET
and so on for free. It will maintain db.json
based on the operations fired.
Wire these in as an npm script in package.json
:
"scripts": {
"start": "run-p start:dev start:api",
"start:dev": "webpack-dev-server --config webpack.config.dev.js --port 3000",
"prestart:api": "node tools/createMockDb.js",
"start:api": "node tools/apiServer.js"
}
prestart
scripts automatically fire before any start
counterparts.
run-p
will run a list of scripts in parallel, for example:
"scripts": {
"start": "run-p start:dev start:api",
"start:dev": "webpack-dev-server --config webpack.config.dev.js --port 3000",
"prestart:api": "node tools/createMockDb.js",
"start:api": "node tools/apiServer.js"
}
Run just the mock API with npm run start:api
or both the API and frontend with npm run start
API Client Wrappers⌗
Its tidy to centralise all API calls under an api
folder. Each API should get its own wrapper, for example courseApi.js
:
import { handleResponse, handleError } from "./apiUtils";
const baseUrl = process.env.API_URL + "/courses/";
export function getCourses() {
return fetch(baseUrl).then(handleResponse).catch(handleError);
}
export function saveCourse(course) {
return fetch(baseUrl + (course.id || ""), {
method: course.id ? "PUT" : "POST", // POST for create, PUT to update when id already exists.
headers: { "content-type": "application/json" },
body: JSON.stringify(course),
})
.then(handleResponse)
.catch(handleError);
}
export function deleteCourse(courseId) {
return fetch(baseUrl + courseId, { method: "DELETE" })
.then(handleResponse)
.catch(handleError);
}
Note the vanilla REST API conventions of PUT
for creates, POST
for updates, DELETE
for deletes and GET
for retrieves.
Having these API wrappers in one place, give better control over environment matters such as the base URL.
const baseUrl = process.env.API_URL + "/courses/";
Webpack can inject this environment, by using the DefinePlugin
in your webpack config:
plugins: [
new webpack.DefinePlugin({
"process.env.API_URL": JSON.stringify("http://localhost:3001"),
}),
Redux Middleware⌗
An extensibility option for redux.
+--------+ +------------+ +---------+
| Action +---->+ Middleware +---->+ Reducer |
+--------+ +------------+ +---------+
A convenient place to hook behaviour onto actions, such as:
- Logging
- Handling API calls
- Crash reporting
- Routing
Here’s an example logger. The signature chains blocks of middleware together, using a technique called currying.
const logger = store => next => action {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd()
return result
}
Reason to use middleware for async:
- Consistency, without middleware the signature of the dispatch calls would vary depending if they were synchronous or asynchronous.
- Purity, avoids binding code to side-effects.
- Testing, components free of side-effects are easier to test.g
Redux Async Libraries⌗
redux-thunk
by Dan Abramov (creator of redux) returns functions from action creator instead of objects.redux-promise
uses promises paired with flux standard actionsredux-observable
dispatches RxJS observablesredux-saga
a full blown async DSL based on ES6 generators
Thunks⌗
Coined from compsci, a thunk is just a function that wraps a function to defer its evalutation.
In this case the call to dispatch is being deferred:
export function deleteVehicle(vehicleId) {
return (dispatch, getState) => {
return VehicleApi.deleteVehicle(vehicleId)
.then(() => {
dispatch(deletedVehicle(vehicleId));
})
.catch(handleError);
};
}
redux-thunks get some built-in power:
- Can access to the store.
- Get
dispatch
injected automatically (hand crafting action creators manually would require manual wire up) - Passed
getState
, allows any conditional check against state (e.g. logged in user) before dispatching (i.e. a conditional dispatch)
The injection of dispatch
is a win, as the function call retains parity with the sync version.
Register redux-thunk with the redux middleware when setting up the store:
import thunk from "redux-thunk";
export default function configureStore(initialState) {
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; //redux devtools
return createStore(
rootReducer,
initialState,
composeEnhancers(applyMiddleware(thunk, reduxImmutableStateInvariant()))
);
Conditional mapStateToProps⌗
In some scenarios the state that get bound to a components props, requires some additional logic. This is common when populating forms for creates versus updates. Setup props with empty new state for a new create, or for an update load the props up with existing data.
Before (sets the course to a new empty object every time):
function mapStateToProps(state) {
return {
course: newCourse,
courses: state.courses,
authors: state.authors,
};
}
After:
export function getCourseBySlug(courses, slug) {
return courses.find((course) => course.slug === slug) || null;
}
function mapStateToProps(state, ownProps) {
const slug = ownProps.match.params.slug;
const course =
slug && state.courses.length > 0
? getCourseBySlug(state.courses, slug)
: newCourse;
return {
course: course,
courses: state.courses,
authors: state.authors,
};
}
mapStateToProps
supports a second optional param called ownProps
, which exposes the components own props, such as the various route data injected by react-router.
The getCourseBySlug
is just filtering data, which is ultimately coming from the redux store. Functions like this become common in the app, and are known as selectors.
When make selectors (unlike this simple example above) consider:
- placing this with reducers for greater reuse
- memoising for perf with a lib like reselect
Polish (the finer things)⌗
Spinner component⌗
The top level component needs to track work in progress (WIP). A simple boolean prop is usually enough. If the prop.loading
is true show the spinner, otherwise, render the normal markup.
render() {
return (
<>
<h2>Courses</h2>
{this.props.loading ? (
<Spinner />
) : (
<>
<button ...
How you determine work in progress is specific to the app. One way to do this is by counting the number of API calls in flight.
function mapStateToProps(state, ownProps) {
return {
loading: state.apiCallsInProgress > 0,
};
}
Redux actions that interact with API’s track this counter by in turn dispatching beginApiCall
, endApiCall
and apiCallError
actions:
export function loadCourses() {
return function (dispatch) {
dispatch(beginApiCall());
return courseApi
.getCourses()
.then((courses) => {
dispatch(loadCoursesSuccess(courses));
})
.catch((error) => {
dispatch(apiCallError());
throw error;
});
};
}
Which as you expect, simply increments or decrements apiCallsInProgress
:
export default function apiCallStatusReducer(
state = initialState.apiCallsInProgress,
action
) {
if (action.type == types.BEGIN_API_CALL) {
return state + 1;
} else if (actionTypeEndsInSuccess(action.type)) {
return state - 1;
} else if (actionTypeEndsInError(action.type)) {
return state - 1;
}
return state;
}
Status API and feedback⌗
Disable UI elements while backend API’s are grinding away. Convey the outcome to the user with toasts.
For localised state, use of redux is overkill. In this component, a local piece of state called saving
is used. setSaving(true)
is set just before saveCourse
is called:
function ManageCoursesPage({
courses,
authors,
loadCourses,
loadAuthors,
saveCourse,
history,
...props
}) {
const [course, setCourse] = useState({ ...props.course });
const [errors, setErrors] = useState({});
const [saving, setSaving] = useState(false);
function handleSave(event) {
event.preventDefault();
if (!formIsValid()) return;
setSaving(true);
saveCourse(course)
.then(() => {
toast.success("Course saved.");
history.push("/courses");
})
.catch((error) => {
setSaving(false);
setErrors({ onSave: error.message });
});
}
This saving
state can be propagated down to individual form components:
<CourseForm
course={course}
errors={errors}
authors={authors}
onChange={handleChange}
onSave={handleSave}
saving={saving}
/>
Which can disable inputs while a save is in progress:
const CourseForm = ({
course,
authors,
onSave,
onChange,
saving = false,
errors = {},
}) => {
return (
<form onSubmit={onSave}>
<h2>{course.id ? "Edit" : "Add"} Course</h2>
{errors.onSave && (
<div className="alert alert-danger" role="alert">
{errors.onSave}
</div>
)}
<TextInput
name="title"
label="Title"
value={course.title}
onChange={onChange}
error={errors.title}
/>
<button type="submit" disabled={saving} className="btn btn-primary">
{saving ? "Saving..." : "Save"}
</button>
</form>
);
};
Server side validation⌗
When interacting with an API, things can go wrong. The promise catch handler should capture the error, in the below example writes an object with a key of onSave
to local state errors
:
function handleSave(event) {
event.preventDefault();
if (!formIsValid()) return;
setSaving(true);
saveCourse(course)
.then(() => {
toast.success("Course saved.");
history.push("/courses");
})
.catch((error) => {
setSaving(false);
setErrors({ onSave: error.message });
});
}
The errors
object is propagated to the downstream form component:
<CourseForm
course={course}
errors={errors}
authors={authors}
onChange={handleChange}
onSave={handleSave}
saving={saving}
/>
Which checks for and conditionally shows any errors.onSave
:
const CourseForm = ({
course,
authors,
onSave,
onChange,
saving = false,
errors = {}
}) => {
return (
<form onSubmit={onSave}>
<h2>{course.id ? "Edit" : "Add"} Course</h2>
{errors.onSave && (
<div className="alert alert-danger" role="alert">
{errors.onSave}
</div>
)}
Client side validation⌗
Store local (non redux) state on the component, that stores a dictionary of errors. Before writing the data in a form to an API, it should be validated (as per formIsValid
below):
function ManageCoursesPage({
courses,
authors,
loadCourses,
loadAuthors,
saveCourse,
history,
...props
}) {
const [course, setCourse] = useState({ ...props.course });
const [errors, setErrors] = useState({});
const [saving, setSaving] = useState(false);
function formIsValid() {
const { title, authorId, category } = course;
const errors = {};
if (!title) errors.title = "Title is required.";
if (!authorId) errors.author = "Author is required.";
if (!category) errors.category = "Category is required.";
setErrors(errors);
return Object.keys(errors).length === 0;
}
function handleSave(event) {
event.preventDefault();
if (!formIsValid()) return;
setSaving(true);
saveCourse(course)
.then(() => {
toast.success("Course saved.");
history.push("/courses");
})
.catch((error) => {
setSaving(false);
setErrors({ onSave: error.message });
});
}
In course form, this object of errors can be passed down. Individual form element can check for the presence of an error that relates to their data (e.g. title or author), and flag to the user that individual inputs are invalid:
const CourseForm = ({
course,
authors,
onSave,
onChange,
saving = false,
errors = {}
}) => {
return (
<form onSubmit={onSave}>
<h2>{course.id ? "Edit" : "Add"} Course</h2>
{errors.onSave && (
<div className="alert alert-danger" role="alert">
{errors.onSave}
</div>
)}
<TextInput
name="title"
label="Title"
value={course.title}
onChange={onChange}
error={errors.title}
/>
<SelectInput
name="authorId"
label="Author"
value={course.authorId || ""}
defaultOption="Select Author"
options={authors.map(author => ({
value: author.id,
text: author.name
}))}
onChange={onChange}
error={errors.author}
/>
<TextInput
name="category"
label="Category"
value={course.category}
onChange={onChange}
error={errors.category}
/>
The low level UI components such as TextInput
show a red validation error if applicable:
const TextInput = ({ name, label, onChange, placeholder, value, error }) => {
let wrapperClass = "form-group";
if (error && error.length > 0) {
wrapperClass += " " + "has-error";
}
return (
<div className={wrapperClass}>
<label htmlFor={name}>{label}</label>
<div className="field">
<input
type="text"
name={name}
className="form-control"
placeholder={placeholder}
value={value}
onChange={onChange}
/>
{error && <div className="alert alert-danger">{error}</div>}
</div>
</div>
);
};
Optimistic deletes⌗
Keep the UI upto date, regardless of the outcome of backend API’s, giving the user a snappy experience.
In the component, can fire the action as soon as the user triggers an event (such as deleting a course):
handleDeleteCourse = (course) => {
toast.success("Course deleted.");
this.props.actions.deleteCourse(course).catch((error) => {
toast.error("Delete failed." + error.message, { autoClose: false });
});
};
The redux action, updates the state (removes a course from the store), and calls the API. Removing the course from the redux store does not hinge on the outcome of the API call:
export function deleteCourse(course) {
return function (dispatch, getState) {
dispatch(deleteCourseOptimistic(course));
return courseApi.deleteCourse(course.id);
};
}
export function deleteCourseOptimistic(course) {
return { type: types.DELETE_COURSE_OPTIMISTIC, course };
}
The reducer does its immutable thing (strips out the course in this case - filter returns a new copy of the original array it iterates over, meeting the immutable requirement of redux):
export default function courseReducer(state = initialState.courses, action) {
switch (action.type) {
case types.DELETE_COURSE_OPTIMISTIC:
return state.filter((course) => course.id !== action.course.id);
default:
return state;
}
}
Testing⌗
Redux Connected Components⌗
When a container component is wired to redux, it is wrapped in a call to connect
right?
export default connect(mapStateToProps, mapDispatchToProps)(CoursesPage);
To test, either wrap the component in <Provider>
in the unit test, or add named export for unconnected version component.
I prefer the first option, as it is more explicit than relying on default exports in the non-test code in the app.
import React from "react";
import { Provider } from "react-redux";
import { createStore } from "redux";
import { mount } from "enzyme";
import { authors, newCourse, courses } from "../../../tools/mockData";
import ManageCoursePage from "./ManageCoursePage";
import rootReducer from "../../redux/reducers";
function render(args) {
const defaultProps = {
authors,
courses,
history: {}, //router
saveCourse: jest.fn(),
loadAuthors: jest.fn(),
loadCourses: jest.fn(),
course: newCourse,
match: { params: {} }, //router
};
const props = { ...defaultProps, ...args };
const store = createStore(rootReducer, {
courses: courses,
authors: authors,
});
return mount(
<Provider store={store}>
<ManageCoursePage {...props} />
</Provider>
);
}
it("sets error when attempting to save an empty title field", () => {
const wrapper = render();
wrapper.find("form").simulate("submit");
const error = wrapper.find(".alert").first();
expect(error.text()).toBe("Title is required.");
});
Note how the store needs to be primed with mock data.
Action Creators⌗
import * as courseActions from "./courseActions";
import * as types from "./actionTypes";
import { courses } from "../../../tools/mockData";
// Test an async action
const middleware = [thunk];
const mockStore = configureMockStore(middleware);
describe("createCourseSuccess", () => {
it("should create a CREATE_COURSE_SUCCESS action", () => {
//arrange
const course = courses[0];
const expectedAction = {
type: types.CREATE_COURSE_SUCCESS,
course,
};
//act
const action = courseActions.createCourseSuccess(course);
//assert
expect(action).toEqual(expectedAction);
});
});
Thunks⌗
import * as courseActions from "./courseActions";
import * as types from "./actionTypes";
import { courses } from "../../../tools/mockData";
import thunk from "redux-thunk";
import fetchMock from "fetch-mock";
import configureMockStore from "redux-mock-store";
const middleware = [thunk];
const mockStore = configureMockStore(middleware);
describe("Async actions", () => {
afterEach(() => {
fetchMock.restore();
});
describe("Load courses thunk", () => {
it("should create BEGIN_API_CALL and LOAD_COURSES_SUCCESS when loading courses", () => {
fetchMock.mock("*", {
body: courses,
headers: { "content-type": "application/json" },
});
const expectedActions = [
{ type: types.BEGIN_API_CALL },
{ type: types.LOAD_COURSES_SUCCESS, courses },
];
const store = mockStore({ courses: [] });
return store.dispatch(courseActions.loadCourses()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
});
Reducers⌗
import courseReducer from "./courseReducer";
import * as actions from "../actions/courseActions";
it("should add course when passed CREATE_COURSE_SUCCESS", () => {
// arrange
const initialState = [
{
title: "A",
},
{
title: "B",
},
];
const newCourse = {
title: "C",
};
const action = actions.createCourseSuccess(newCourse);
// act
const newState = courseReducer(initialState, action);
// assert
expect(newState.length).toEqual(3);
expect(newState[0].title).toEqual("A");
expect(newState[1].title).toEqual("B");
expect(newState[2].title).toEqual("C");
});
Store⌗
import { createStore } from "redux";
import rootReducer from "./reducers";
import initialState from "./reducers/initialState";
import * as courseActions from "./actions/courseActions";
it("Should handle creating courses", function () {
// arrange
const store = createStore(rootReducer, initialState);
const course = {
title: "Clean Code",
};
// act
const action = courseActions.createCourseSuccess(course);
store.dispatch(action);
// assert
const createdCourse = store.getState().courses[0];
expect(createdCourse).toEqual(course);
});