DEV Community

Robert Chen
Robert Chen

Posted on • Originally published at Medium on

2

Learn redux-optimist

Simplify optimistic rendering with redux-optimist library

The redux-optimist library has been extremely helpful to me and I’d like to share that with you. I’ve designed a simple tutorial where I’ll walk you through how to set up and use the middleware. We’re going to use a sweet pokemon API as our demo practice. We’ll fetch this Pikachu and optimistically evolve it to Raichu. If the request fails, then we’ll devolve back to Pikachu.

1) Let’s install the dependencies we need, in your terminal:
yarn create react-app app-name
cd app-name
yarn add react-dom
yarn add react-router-dom
yarn add react-redux
yarn add redux
yarn add redux-thunk
yarn add lodash

2) Follow along to set up Redux, or skip ahead to step 3 if you have your own preferred redux setup.

a. open src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import thunk from 'redux-thunk';
import rootReducer from './reducer';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(thunk))
);
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
view raw index.js hosted with ❤ by GitHub

b. create action file, in your terminal: touch src/pokemonActions.js && open src/pokemonActions.js

export const GET_PIKACHU = 'GET_PIKACHU';
export const GET_RAICHU = 'GET_RAICHU';
export const GET_RAICHU_FAILED = 'GET_RAICHU_FAILED';
export const getPikachu = () => {
return dispatch => {
return fetch('https://pokeapi.co/api/v2/pokemon/pikachu/')
.then(res => res.json())
.then(pikachu => {
dispatch({
type: GET_PIKACHU,
payload: { pokemon: pikachu }
});
})
.catch(console.error);
};
};
export const getRaichu = () => {
return dispatch => {
setTimeout(() => {
return fetch('https://pokeapi.co/api/v2/pokemon/raichu/')
.then(res => res.json())
.then(raichu => {
dispatch({
type: GET_RAICHU,
payload: { pokemon: raichu }
});
})
.catch(error => {
dispatch({
type: GET_RAICHU_FAILED,
payload: { error }
});
});
}, 1000);
};
};

c. create reducer file, in your terminal: touch src/reducer.js && open src/reducer.js

import { GET_PIKACHU, GET_RAICHU, GET_RAICHU_FAILED } from './pokemonActions';
const initialState = { pokemon: [] };
const reducer = (state = initialState, action) => {
switch (action.type) {
case GET_PIKACHU:
case GET_RAICHU: {
return { ...state, pokemon: action.payload.pokemon };
}
case GET_RAICHU_FAILED: {
return { ...state, error: action.payload.error };
}
default:
return state;
}
};
export default reducer;
view raw reducer.js hosted with ❤ by GitHub

d. open src/App.js

import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { get } from 'lodash';
import { getPikachu, getRaichu } from './pokemonActions';
import './App.css';
class App extends React.Component {
componentDidMount() {
const { getPikachu } = this.props;
getPikachu();
}
renderStartOverButton() {
const { pokemon, getPikachu } = this.props;
const pokemonName = get(pokemon, 'forms[0].name');
if (pokemonName === 'pikachu') {
return null;
}
return (
<div className="start-over-button">
<button onClick={getPikachu}>Start Over</button>
</div>
);
}
renderError() {
const { error } = this.props;
if (!error) {
return null;
}
return (
<h6 className="error">{`${error}, pikachu doesn't want to evolve!`}</h6>
);
}
render() {
const { pokemon, getRaichu, error } = this.props;
const pokemonImg = get(pokemon, 'sprites.front_default');
if (!pokemonImg && !error) {
return <div>Loading . . .</div>;
}
const pokemonName = get(pokemon, 'name');
let message = (
<span>
<p>Click to evolve</p>
<img
src="https://cdn.bulbagarden.net/upload/thumb/e/e7/Thunder_Stone_BW135.png/250px-Thunder_Stone_BW135.png"
className="thunder-stone"
alt="thunder-stone"
></img>
</span>
);
let animationClass = '';
if (pokemonName === 'raichu') {
message = <p>Congrats on your new Raichu!</p>;
animationClass = 'grow';
}
return (
<div className="container">
{this.renderStartOverButton()}
<div onClick={getRaichu}>
<img
src={pokemonImg}
className={animationClass}
alt="pokemon"
/>
<h6>{message}</h6>
</div>
{this.renderError()}
</div>
);
}
}
const mapStateToProps = state => {
return {
pokemon: state.pokemon,
error: state.error
};
};
const mapDispatchToProps = dispatch => {
return {
getPikachu: bindActionCreators(getPikachu, dispatch),
getRaichu: bindActionCreators(getRaichu, dispatch)
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
view raw App.js hosted with ❤ by GitHub

e. open src/App.css

.container {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
text-align: center;
}
.start-over-button,
.error {
position: absolute;
top: 60%;
}
.error {
font-size: 0.5em;
color: red;
}
.grow {
animation: grow 0.5s ease;
}
.thunder-stone {
width: 50px;
}
@keyframes grow {
from {
transform: scale(0);
}
to {
transform: scale(1);
}
}
view raw App.css hosted with ❤ by GitHub

3) Let’s install redux-optimist now, in your terminal:
yarn add redux-optimist

4) I suggest commiting here, that way you can see your git diff before implementing redux-optimist and after redux-optimist:
git add . && git commit -m "feat(redux): finished setting up redux"

5) Modify our pokemonActions.js to create new actions and pass some responsibility to the redux-optimist library:

export const GET_PIKACHU = 'GET_PIKACHU';
export const GET_RAICHU = 'GET_RAICHU';
export const GET_RAICHU_BEGIN = 'GET_RAICHU_BEGIN'; // new action
export const GET_RAICHU_COMPLETE = 'GET_RAICHU_COMPLETE'; // new action
export const GET_RAICHU_FAILED = 'GET_RAICHU_FAILED';
export const getPikachu = () => {
return dispatch => {
return fetch('https://pokeapi.co/api/v2/pokemon/pikachu/')
.then(res => res.json())
.then(pikachu => {
console.log('getPikachu(), dispatch GET_PIKACHU');
dispatch({
type: GET_PIKACHU,
payload: { pokemon: pikachu }
});
})
.catch(console.error);
};
};
export const getRaichu = () => {
return dispatch => {
console.log('getRaichu(), dispatch GET_RAICHU');
dispatch({
type: GET_RAICHU // let middleware take it from here, we're going to create a file that receives this action
});
};
};

6) Create a middleware folder and create this file getRaichu.js inside the folder, in your terminal: mkdir src/middleware && touch src/middleware/getRaichu.js && open src/middleware/getRaichu.js

import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
import {
GET_RAICHU,
GET_RAICHU_BEGIN,
GET_RAICHU_COMPLETE,
GET_RAICHU_FAILED
} from '../pokemonActions';
let nextTransactionID = 0;
export default function(store) {
return next => action => {
if (action.type !== GET_RAICHU) {
return next(action);
}
let transactionID = nextTransactionID++;
const raichu = {
name: 'raichu',
sprites: {
front_default:
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/26.png'
}
};
console.log('GET_RAICHU_BEGIN')
next({
type: GET_RAICHU_BEGIN,
payload: { pokemon: raichu, error: '' },
optimist: { type: BEGIN, id: transactionID }
});
setTimeout(() => {
// toggle fetch for successful or failed request
fetch('https://pokeapi.co/api/v2/pokemon/raichu/')
// fetch('https://pokeapifailedrequest.co/api/v2/pokemon/raichu/')
.then(res => res.json())
.then(raichu => {
console.log('GET_RAICHU_COMPLETE');
next({
type: GET_RAICHU_COMPLETE,
payload: { pokemon: raichu, error: '' },
optimist: { type: COMMIT, id: transactionID }
});
})
.catch(error => {
console.log('GET_RAICHU_FAILED');
next({
type: GET_RAICHU_FAILED,
payload: { error: error.message },
optimist: { type: REVERT, id: transactionID }
});
});
}, 1000); // simulate a request that takes 1 second
};
}
view raw getRaichu.js hosted with ❤ by GitHub

7) Import and use the getRaichu.js middleware in our index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import thunk from 'redux-thunk';
import rootReducer from './reducer';
import getRaichu from './middleware/getRaichu'; // import middleware file
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(thunk, getRaichu)) // use middleware file we created
);
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
view raw index.js hosted with ❤ by GitHub

8) Modify our reducer.js to work with the new actions and middleware:

import optimist from 'redux-optimist';
import {
GET_PIKACHU,
GET_RAICHU_BEGIN, // new action
GET_RAICHU_COMPLETE, // new action
GET_RAICHU_FAILED
} from './pokemonActions';
const initialState = { pokemon: [], error: '' };
const reducer = (state = initialState, action) => {
switch (action.type) {
case GET_PIKACHU: {
console.log('GET_PIKACHU');
return { ...state, pokemon: action.payload.pokemon };
}
case GET_RAICHU_BEGIN: { // optimistically update store immediately
console.log('GET_RAICHU_BEGIN:', action.payload);
return {
...state,
pokemon: action.payload.pokemon,
error: action.payload.error
};
}
case GET_RAICHU_COMPLETE: { // really update store if request succeeds
console.log('GET_RAICHU_COMPLETE:', action.payload);
return {
...state,
pokemon: action.payload.pokemon,
error: action.payload.error
};
}
case GET_RAICHU_FAILED: { // remove what was optimistically updated to the store if request fails and save the error to the store instead
console.log('GET_RAICHU_FAILED:', action.payload);
return { ...state, error: action.payload.error };
}
default:
return state;
}
};
export default optimist(reducer);
view raw reducer.js hosted with ❤ by GitHub

9) Now in your terminal, yarn start and open up your console. I’ve placed a couple console.log to help observe the procedure of our action, middleware, and reducer.

GET_RAICHU_COMPLETE

You’ll notice that the object in GET_RAICHU_BEGIN is a mock Raichu where I only supplied the name and image, then when the request succeeds, GET_RAICHU_COMPLETE sends the full object returned from the API to update our store.

10) Now let’s imitate a failed request by commenting out the fetch request on line 32 in getRaichu.js and commenting in line 33.

GET_RAICHU_FAILED

This time you will see that we optimistically render Raichu until the request comes back as a fail. The store will automatically revert back to Pikachu. At this time we also capture the error from the failed request and display it to the user. So moral of the story, don’t evolve your Pikachu :)


Here are screenshots of the git diff from our usual redux to implementing redux-optimist:

index.js diff
pokemonActions.js diff
getRaichu.js diff
reducer.js diff

That’s it for optimistic rendering with the redux-optimist library! Hope this was helpful!


Bring your friends and come learn JavaScript in a fun never before seen way! waddlegame.com

Image of Datadog

How to Diagram Your Cloud Architecture

Cloud architecture diagrams provide critical visibility into the resources in your environment and how they’re connected. In our latest eBook, AWS Solution Architects Jason Mimick and James Wenzel walk through best practices on how to build effective and professional diagrams.

Download the Free eBook

Top comments (0)