Simple Example
This example is a very simple web app that has only one feature – you can view and update your username. The purpose of this example is to demonstrate how requests and mutations (including optimistic updates) work with redux-query.
Note: This example fakes a server with a custom mock network interface. In a real app, you would want to use a network interface that actually communicates to a server via HTTP.
Entry point
index.js
import React from 'react'; import { render } from 'react-dom'; import App from './components/App'; import store from './store'; render(<App store={store} />, document.getElementById('root'));
Redux store
store.js
import { applyMiddleware, createStore, combineReducers } from 'redux'; import { entitiesReducer, queriesReducer, queryMiddleware } from 'redux-query'; import mockNetworkInterface from './mock-network-interface'; export const getQueries = state => state.queries; export const getEntities = state => state.entities; const reducer = combineReducers({ entities: entitiesReducer, queries: queriesReducer, }); const store = createStore( reducer, applyMiddleware(queryMiddleware(mockNetworkInterface, getQueries, getEntities)), ); export default store;
Query configs
query-configs/name.js
export const nameRequest = () => { return { url: `/api/name`, update: { name: (prev, next) => next, }, }; }; export const changeNameMutation = (name, optimistic) => { const queryConfig = { url: `/api/change-name`, body: { name, }, update: { name: (prev, next) => next, }, }; if (optimistic) { queryConfig.optimisticUpdate = { name: () => name, }; } return queryConfig; };
Selectors
selectors/name.js
export const getName = state => state.entities.name;
Components
components/ChangeUsernameForm.js
import * as React from 'react'; import { useSelector } from 'react-redux'; import { useRequest, useMutation } from 'redux-query-react'; import * as nameQueryConfigs from '../query-configs/name'; import * as nameSelectors from '../selectors/name'; const ChangeUsernameForm = props => { const [inputValue, setInputValue] = React.useState(''); const [status, setStatus] = React.useState(null); const [error, setError] = React.useState(null); const username = useSelector(nameSelectors.getName); useRequest(nameQueryConfigs.nameRequest()); const [queryState, changeName] = useMutation(optimistic => nameQueryConfigs.changeNameMutation(inputValue, optimistic), ); const submit = React.useCallback( optimistic => { changeName(optimistic).then(result => { if (result !== 200) { setError(result.text); } setStatus(result.status); }); }, [changeName], ); const isPending = queryState.isPending; return ( <div> <h2>Current username</h2> <p>{username || <em>(no username set)</em>}</p> <h2>Change username</h2> <form onSubmit={e => { // Prevent default form behavior. e.preventDefault(); }} > <input type="text" value={inputValue} placeholder="Enter a new username" disabled={isPending} onChange={e => { setInputValue(e.target.value); }} /> <input type="submit" value="Submit" onClick={() => submit(false)} disabled={isPending} /> <input type="submit" value="I'm Feeling Optimistic" onClick={() => submit(true)} disabled={isPending} /> {isPending ? ( <p>Loading…</p> ) : ( typeof status === 'number' && ( <> {status === 200 ? ( <p>Success!</p> ) : ( <p> An error occurred: " {error} ". </p> )} </> ) )} </form> </div> ); }; export default ChangeUsernameForm;
components/App.js
import * as React from 'react'; import { Provider } from 'react-redux'; import { Provider as ReduxQueryProvider } from 'redux-query-react'; import ChangeUsernameForm from '../components/ChangeUsernameForm'; import { getQueries } from '../store'; const Intro = () => { return ( <> <h1>Instructions</h1> <p> This example is a very simple web app that has only one feature – you can view and update your username. The purpose of this example is to demonstrate how requests and mutations (including optimistic updates) work with redux-query. </p> <ol> <li> Pretend that you have used this app before. Wait for your username to load under the "Current username" header. </li> <li>Enter a username into the text input field.</li> <li> Click "Submit" and wait for the text you entered to be accepted and reflected under the "Current username" header. </li> <li>Clear the text input field to be empty.</li> <li> Click "Submit" and wait for the error message. Note this did not change the current username that is displayed below the "Current username" header. </li> <li>Enter a new username into the text input field.</li> <li> Click "I'm Feeling Optimistic" and see how the current username displayed under the "Current username" header updates immediately. </li> <li>Clear the text input field to be empty.</li> <li> Click "I'm Feeling Optimistic" and see how the current username displayed under the "Current username" header updates immediately. Then a second later when the mutation finishes, see how the username reverts to the previous value. </li> </ol> </> ); }; const App = props => { return ( <Provider store={props.store}> <ReduxQueryProvider queriesSelector={getQueries}> <Intro /> <hr /> <ChangeUsernameForm /> </ReduxQueryProvider> </Provider> ); }; export default App;
Custom mock network interface
mock-network-interface.js
const artificialDelayDuration = 1000; // Fake database to record the name const memoryDb = { name: 'jhalpert78', }; const mockNetworkInterface = (url, method, { body }) => { let timeoutId = null; return { abort() { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } }, execute(callback) { if (url.match(/^\/api\/name/)) { // Endpoint for getting the current name if (method.toUpperCase() === 'GET') { timeoutId = setTimeout(() => { callback(null, 200, { name: memoryDb.name, }); }, artificialDelayDuration); } else { callback(null, 405); } } else if (url.match(/^\/api\/change-name/)) { // Endpoint for changing the name if (method !== 'POST') { callback(null, 405); return; } if (!body.name) { timeoutId = setTimeout(() => { callback(null, 400, null, 'Username cannot be empty'); }, artificialDelayDuration); return; } if (body.name.trim() !== body.name || !body.name.match(/^[a-zA-Z0-9]+$/)) { timeoutId = setTimeout(() => { callback( null, 400, null, 'A valid username must only contain alphanumerics with no leading or trailing spaces', ); }, artificialDelayDuration); return; } memoryDb.name = body.name; timeoutId = setTimeout(() => { const responseBody = { name: memoryDb.name, }; callback(null, 200, responseBody, JSON.stringify(responseBody)); }, artificialDelayDuration); } else { callback(null, 404); } }, }; }; export default mockNetworkInterface;