Goal: build our actual app with redux and redux-saga
npm install --save-dev babel-plugin-transform-async-to-generator babel-plugin-transform-runtime babel-preset-stage-3 react-redux redux redux-saga whatwg-fetch
babel-plugin-transform-async-to-generator
: to use async/awaitbabel-plugin-transform-runtime
: in a real production environment we'd only use this for tests, and babel-preset-env + useBuiltIns + babel-polyfill for the actual bundle, but for simplicity's sake we'll just use this one here on its own - please do look into babel-preset-envbabel-preset-stage-3
: for object rest spread ({...}
)react-redux
: to connect redux state to react componentsredux-saga
: to handle side effects in our app (e.g. fetching from the github API)
- Add
stage-3
preset andtransform-runtime
plugin to our.babelrc
config:{ "presets": ["es2015", "stage-3", "react"], "plugins": [ ["transform-runtime", { "helpers": false, "polyfill": false, "regenerator": true }] ] }
- Start with the action creators - create
src/actions/repos.js
:export const types = { ERROR_REQUESTING_REPOS: 'ERROR_REQUESTING_REPOS', RECEIVED_REPOS: 'RECEIVED_REPOS', REQUEST_REPOS: 'REQUEST_REPOS' }; export const errorRequestingRepos = (error) => ({ payload: { error }, type: types.ERROR_REQUESTING_REPOS }); export const receivedRepos = (repos) => ({ payload: { repos }, type: types.RECEIVED_REPOS }); export const requestRepos = (organization) => ({ payload: { organization }, type: types.REQUEST_REPOS });
- Now the reducer - create
src/reducers/repos.js
:import {types} from '../actions/repos'; const { ERROR_REQUESTING_REPOS, RECEIVED_REPOS, REQUEST_REPOS } = types; export const initialState = { error: null, fetching: false, repos: null }; export default function repos (state = initialState, action) { switch (action.type) { case ERROR_REQUESTING_REPOS: return { ...state, error: action.payload.error }; case RECEIVED_REPOS: return { ...state, fetching: false, repos: action.payload.repos }; case REQUEST_REPOS: return { ...state, fetching: true, repos: null }; default: return state; } }
- And a root reducer which won't make much sense here, but would combine multiple ones in a real app - create
src/reducers/index.js
:import {combineReducers} from 'redux'; import repos from './repos'; const reducers = combineReducers({ repos }); export default reducers;
- Add a
fetch
polyfill to our app - modifysrc/index.js
and add this to the very top:// eslint-disable-next-line import/no-unassigned-import import 'whatwg-fetch';
- Let's create an API for fetching the repos - create
src/api/repos.js
:export const fetchByOrg = async (organization) => { const endpoint = `https://api.github.com/orgs/${organization}/repos`; const response = await fetch(endpoint); const json = await response.json(); if (response.status < 200 || response.status >= 400) { const error = new Error(json && json.message || response.statusText); error.response = response; throw error; } return json.map( // eslint-disable-next-line id-match ({name, html_url}) => ({ name, url: html_url }) ); };
- Now it's time to create our saga - create
src/sagas/repos.js
:import {call, fork, put, takeLatest} from 'redux-saga/effects'; import {fetchByOrg} from '../api/repos'; import { errorRequestingRepos, receivedRepos, types } from '../actions/repos'; export const fetchReposFromApi = function *(action) { try { const {payload: {organization}} = action; const reposFromApi = yield call(fetchByOrg, organization); yield put(receivedRepos(reposFromApi)); } catch (error) { yield put(errorRequestingRepos(error)); } }; const watchRequestRepos = function *() { yield takeLatest(types.REQUEST_REPOS, fetchReposFromApi); }; export default function *repos () { yield [ fork(watchRequestRepos) ]; }
- Next, we create a root saga, much like the root reducer - create
src/sagas/index.js
:import {fork} from 'redux-saga/effects'; import repos from './repos'; export default function *sagas () { yield [ fork(repos) ]; }
- The only thing left for our redux side of the app is, first, creating a store - create
src/store/configureStore.js
:import {applyMiddleware, createStore, compose} from 'redux'; import createSagaMiddleware from 'redux-saga'; import rootReducer from '../reducers'; import rootSaga from '../sagas'; export default function configureStore (preloadedState) { const sagaMiddleware = createSagaMiddleware(); const store = createStore( rootReducer, preloadedState, compose( applyMiddleware(sagaMiddleware), window && window.devToolsExtension ? window.devToolsExtension() : (passThrough) => passThrough ) ); sagaMiddleware.run(rootSaga); return store; }
- And, finally, wiring it into our app via the
react-redux
Provider
HOC - modifysrc/index.js
:// eslint-disable-next-line import/no-unassigned-import import 'whatwg-fetch'; import React from 'react'; import {render} from 'react-dom'; import {Provider} from 'react-redux'; import configureStore from './store/configureStore'; import styles from './styles.scss'; const store = configureStore(); render( <Provider store={store}> <div className={styles.container}>Hello world!</div> </Provider>, document.querySelector('[data-react-workshop]') );
We now have a perfectly workable Redux state tree, with sagas running... except we have no UI components to:
- Dispatch the action that will trigger the saga that fetches the repos, and
- Display the information conveyed by the state tree to the user
So let's create some components to do just that.
- Create
src/components/GetReposButton/index.js
:import React from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; import {requestRepos} from '../../actions/repos'; import styles from './styles.scss'; const HARDCODED_ORG_NAME = 'github'; const GetReposButton = ({dispatch, fetching}) => { const handleOnClick = () => dispatch(requestRepos(HARDCODED_ORG_NAME)); return ( <button className={styles.button} disabled={fetching} onClick={!fetching && handleOnClick} > Get Repos </button> ); }; GetReposButton.propTypes = { dispatch: PropTypes.func.isRequired, fetching: PropTypes.bool.isRequired }; const mapStateToProps = (state) => ({ fetching: state.repos.fetching }); export {GetReposButton as GetReposButtonPureComponent}; export default connect(mapStateToProps)(GetReposButton);
- And its corresponding
src/components/GetReposButton/styles.scss
:.button { background: #4cf; font-weight: bold; border: 0; appearance: none; padding: 16px; &[disabled] { background: #999; color: white; } }
- Now to display some results - create
src/components/RepoList/index.js
:import React from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; const RepoList = ({fetching, repos}) => { if (fetching) { return <p>Loading...</p>; } if (!repos) { return <p>No results</p>; } return ( <ul> { repos.map( ({name, url}) => <li key={name}> <a href={url}>{name}</a> </li> ) } </ul> ); }; RepoList.propTypes = { fetching: PropTypes.bool.isRequired, repos: PropTypes.array }; RepoList.defaultProps = { repos: null }; const mapStateToProps = (state) => ({ fetching: state.repos.fetching, repos: state.repos.repos }); export {RepoList as RepoListPureComponent}; export default connect(mapStateToProps)(RepoList);
- And now we just wire them together into the root app component - modify
src/index.js
:// eslint-disable-next-line import/no-unassigned-import import 'whatwg-fetch'; import React from 'react'; import {render} from 'react-dom'; import {Provider} from 'react-redux'; import GetReposButton from './components/GetReposButton'; import RepoList from './components/RepoList'; import configureStore from './store/configureStore'; import styles from './styles.scss'; const store = configureStore(); render( <Provider store={store}> <div className={styles.container}> <GetReposButton /> <RepoList /> </div> </Provider>, document.querySelector('[data-react-workshop]') );
- Run
npm run start
and openlocalhost:8080
(or whichever port) - click on the button - Congratulations! You should have a fully running react/redux/redux-saga application there, unless something went horribly wrong.
Keep in mind that there's millions of equally valid ways to create a react/redux application - the flexibility offered by these libraries is immense. You'll have to experiment and pick and choose to find what works for you.
Now, no application is complete without tests - so let's see to that in Step 4