Okay, so we have a basic application, but it doesn't do much.
Let's get cooking. We'll start by getting things wired up with Redux Thunk.
import thunk from 'redux-thunk';
// …
const store = createStore(reducer, applyMiddleware(thunk));
Again—I like to start with my actions. We're going to need to hit an API, but let's just start by making sure stuff works.
export const FETCH_CHARACTERS_FULFILLED = 'FETCH_CHARACTERS_FULFILLED';
export const fetchCharacters = (searchTerm) => {
return {
type: FETCH_CHARACTERS_FULFILLED,
payload: { searchTerm },
};
};
And I'll add a console.log
to actions.js
for good measure.
const charactersReducer = (characters = [], action) => {
console.log(action);
return characters;
};
Alright, now that we have that in place, let's connect FetchCharacters
to talk to Redux.
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { fetchCharacters } from './actions';
const FetchCharacters = ({ fetchCharacters }) => {
// …
};
export default connect(
null,
{ fetchCharacters },
)(FetchCharacters);
Boom! If I check the console, I can see that the action is firing. Let's turn that action creator into a thunk creator!
import { search } from './api';
export const FETCH_CHARACTERS_FULFILLED = 'FETCH_CHARACTERS_FULFILLED';
export const fetchCharacters = (searchTerm) => {
return (dispatch) => {
fetch(search + searchTerm)
.then((response) => response.json())
.then((response) => response.results)
.then((characters) => {
dispatch({
type: FETCH_CHARACTERS_FULFILLED,
payload: { characters },
});
});
};
};
We can now set up that result in the reducer.js
.
const charactersReducer = (characters = [], action) => {
if (action.type === FETCH_CHARACTERS_FULFILLED) {
return action.payload.characters;
}
return characters;
};
Looks like we're getting everything in the console. Now, let's hook it up to the Characters
component.
import React from 'react';
import { connect } from 'react-redux';
import Character from './Character';
const Characters = ({ characters = [] }) => {
return (
<section className="Characters">
{characters.map((character) => (
<Character key={character.id} character={character} />
))}
</section>
);
};
const mapStateToProps = (state) => {
return { characters: state.characters };
};
export default connect(mapStateToProps)(Characters);
Okay, first—we need to disentangle our actions:
import { search } from './api';
export const FETCH_CHARACTERS = 'FETCH_CHARACTERS';
export const FETCH_CHARACTERS_FULFILLED = 'FETCH_CHARACTERS_FULFILLED';
export const fetchCharacters = (searchTerm) => {
return {
type: FETCH_CHARACTERS,
payload: { searchTerm },
};
};
export const fetchCharactersFulfilled = (payload) => {
return {
type: FETCH_CHARACTERS_FULFILLED,
payload,
};
};
Okay, now—let's write out the epic:
import { ajax } from 'rxjs/ajax';
import { ofType } from 'redux-observable';
import { map, mergeMap } from 'rxjs/operators';
import { FETCH_CHARACTERS, fetchCharactersFulfilled } from './actions';
import { search } from './api';
const fetchCharactersEpic = (action$) =>
action$.pipe(
ofType(FETCH_CHARACTERS),
mergeMap((action) =>
ajax
.getJSON(search + action.payload.searchTerm)
.pipe(map((response) => fetchCharactersFulfilled(response.results))),
),
);
export default fetchCharactersEpic;
Alright: let's hook it up:
import { createEpicMiddleware } from 'redux-observable';
import rootEpic from './epic';
// …
const epicMiddleware = createEpicMiddleware();
const store = createStore(reducer, applyMiddleware(epicMiddleware));
epicMiddleware.run(rootEpic);
const fetchCharactersEpic = (action$) =>
action$.pipe(
ofType(FETCH_CHARACTERS),
mergeMap((action) =>
ajax.getJSON(search + action.payload.searchTerm).pipe(
map((response) => fetchCharactersFulfilled(response.results)),
takeUntil(
action$.pipe(
tap((value) => console.log('Cancelling', value)),
ofType(FETCH_CHARACTERS),
),
),
),
),
);