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;