UNPKG

9.43 kBMarkdownView Raw
1# Item (Redux Template)
2
3The standard way to handle an async piece of data in Redux store.
4
5**Why?** — It is a common need to keep in Redux store a piece of data that
6is asyncroneously fetched from some API, or generated some other way, and should
7be updated periodically. **item** actions and reducer provided by our library
8implement a good way to handle this.
9
10### Content
11- [Overview](#overview)
12- [Tutorial](#tutorial)
13
14### Overview
15
16Say you need to load (generate) and keep in Redux store some piece of `DATA`,
17which can be of any JS type (Boolean, Number, String, Object, etc.). To handle
18it efficiently you want:
19
20- Record whether `DATA` are being loaded currently, and being able to cancel
21 pending requests to load `DATA`, if necessary;
22
23- Keep track of the last `DATA` update time, to refresh them when necessary,
24 while avoiding to reload them more than needed;
25
26- Keep track whether these data are used, to be able to drop them out of the
27 store, when they are of no use anymore.
28
29Our solution for this problem comes in two pieces:
30
31- [**item reducer**](auto/reducers.item.md) manages a segment of Redux store
32 that keeps your `DATA` in the following way:
33 ```js
34 {
35 data: DATA,
36 loadingOperationId,
37 numRefs,
38 timestamp,
39 }
40 ```
41 In addition to the actual `DATA`, kept under the **data** field, the segment
42 stores:
43 - **loadingOperationId** — `null` when data are not being loaded,
44 otherwises a unique ID of the loading operation. During an ongoing data
45 loading operation you can change this ID, to silently ignore the result
46 of the pending operation; thus cancelling it, or overriding with a new
47 data request.
48 - **numRefs** — Allows you to count, if you wish, refences to `DATA`
49 from different objects in your code; to support a simple garbage collection
50 mechanics.
51 - **timestamp** — Timestamp [ms] of the latest update of `DATA`. Helps
52 to reload stale data only when they are really outdated.
53
54- [**item actions**](auto/actions.item.md) provide the control interface for
55 the **item** reducer:
56 - Allow to (re-)load data into the store segment;
57 - Update reference counter;
58 - Remove stale data with no references to them.
59
60### Tutorial
61
62Say, you want to add `myData` segment to your Redux store, using **item**
63actions / reducers. You want multiple ReactJS containers rely on this segment,
64and you want to properly clean-up stale data when no container is using them.
65
66For `myData` actions module you should do:
67```js
68// src/shared/actions/myData.js
69
70import { actions, redux } from 'topcoder-react-utils';
71
72const itemActions = actions.item;
73
74// This is the only payload creator you have to customize, to implement
75// the actual loading of your data. Here `...args` stay for any arguments
76// you need to get your data.
77async function getDone(operationId, ...args) {
78 // Here is your async operation(s) that produces the data you need.
79 const data = await ...;
80 // Here you proxy the result to the standard item's action:
81 return redux.proxyAction(itemActions.loadItemDone)(operationId, data);
82}
83
84export default redux.createActions({
85 MY_DATA: {
86 CLEAN_UP: redux.proxyAction(itemActions.dropData),
87 GET_INIT: redux.proxyAction(itemActions.loadItemInit),
88 GET_DONE: getDone,
89 UPDATE_REFERENCE_COUNTER:
90 redux.proxyAction(itemActions.updateReferenceCounter),
91 },
92});
93```
94
95Now, you create `myData` reducer the following way (the layout of module follows
96our standard practice):
97```js
98// src/shared/reducers/myData.js
99
100import actions from 'actions/myData';
101import { actions as truActions, reducers, redux } from 'topcoder-react-utils';
102
103const itemActions = truActions.item;
104const itemReducer = reducers.item;
105
106/* Custom reducer function to handle possible errors in a custom way. */
107function onGetDone(state, action) {
108 if (action.error) {
109 // SOME ERROR HANDLING HERE.
110 return state;
111 }
112 const a = redux.proxyAction(itemActions.loadDataDone, action);
113 return itemReducer(state, a);
114 // Note, the code above is equivalent to the following one, but the former
115 // one is a bit more efficient under the hood:
116 // ```
117 // const r = redux.proxyReducer(itemReducer, itemActions.loadDataDone);
118 // return r(state, action);
119 // ```
120}
121
122/**
123 * Creates a new reducer.
124 * @param {Object} intialState Optional. Initial state.
125 * @return {Reducer}
126 */
127function create(initialState = itemReducer(undefined, '@@INIT')) {
128 const a = actions.myData;
129 return redux.handleActions({
130 [a.cleanUp]: redux.proxyReducer(itemReducer, itemActions.dropData),
131 [a.getInit]: redux.proxyReducer(itemReducer, itemActions.loadDataInit),
132 [a.getDone]: onGetDone,
133 [a.updateReferenceCounter]:
134 redux.proxyReducer(itemReducer, itemActions.updateReferenceCounter),
135 }, initialState);
136}
137
138/**
139 * Reducer factory, that may use some `options` to generate initial state
140 * more appropriate for the user request, thus taking care about a better
141 * server-side rendering.
142 * @param {Object} options Optional. SSR options.
143 * @return {Promise} Resolves to the reducer.
144 */
145export async function factory(options) {
146 if (options) {
147 let initialState
148 // PREPARES A CUSTOMIZED initialState HERE.
149 return create(initialState)
150 }
151 return create();
152}
153
154/* Reducer with the default initial state. */
155export default create();
156```
157
158Now, provided that you have embed this reducer into your root reducer in
159the regular way, you can follow the following pattern for a container that
160relies on these data:
161```js
162// src/shared/containers/example.js
163
164import actions from 'actions/myData';
165import Example from 'components/Example';
166import PT from 'prop-types';
167import React from 'react';
168import shortId from 'shortid';
169import { redux } from 'topcoder-react-utils';
170
171const MAX_AGE = 5 * 60 * 1000; // 5 min.
172const RELOAD_AGE = 1 * 60 * 1000; // 1 min.
173
174class ExampleContainer extends React.Component {
175 componentDidMount() {
176 const {
177 dataTimestamp,
178 load,
179 loadingData,
180 updateReferenceCounter,
181 } = this.props;
182 const now = Date.now();
183
184 /* If data are old enough and are not being loaded currently,
185 * we trigger data reload. */
186 if (!loadingData && dataTimestamp < now - RELOAD_AGE) {
187 load(/* In most cases you'll pass in you own arguments, specifying
188 * what data should be loaded, etc. */);
189 }
190
191 /* In all cases we update data reference counter, to keep track that
192 * a newly updated component relies on the data now. */
193 updateReferenceCounter(1);
194 }
195
196 componentDidUpdate() {
197 /* In case you need update data in response to a prop change,
198 * you can do it here. You should compare current props to the
199 * new props to decide, whether data refresh is necessary, and
200 * then use load(..) prop to trigger the reload. No need to update
201 * the data reference counter. */
202 }
203
204 componentWillUnmount() {
205 const = { cleanUp, updateReferenceCounter } = this.props;
206 /* Mark that this container will not use the data anymore. */
207 updateReferenceCounter(-1);
208 /* This call will drop data out of the state if it was the last
209 * container to rely on them. */
210 cleanUp();
211 /* In case you want keep data in Redux for some more time (to be able
212 * to use them in a newly created container), you can do: */
213 const olderThan = Date.now() - MAXAGE;
214 cleanUp(olderThan);
215 /* Notice that in this case, if data are not removed by the last
216 * container that uses them, they'll stay in the state until another
217 * container is created and tries to clean them up on its destruction.
218 * In case it is not good for performance of you app, you can implement
219 * and fire here an async cleanUp operation, that will delay cleanUp to
220 * some point in future. */
221 }
222
223 render() {
224 /* In most cases, these are the only data you want to forward into
225 * the actual rendering function. */
226 const { data, loadingData } = this.props;
227
228 /* It is up to you, what you want to do when `data` are old,
229 * and the new data loading operation is already going on. However,
230 * for a better user experience, in most of cases, it makes sense to
231 * show old data anyway, and probably even do not show the reloading
232 * indicator, thus making a silent data update. Depending on the nature
233 * of you data and UI you might prefer different strategies. */
234 return <Example data={data} loading={loadingData} />;
235 }
236}
237
238ExampleContainer.defaultProps = {
239 data: null,
240};
241
242ExampleContainer.propTypes = {
243 cleanUp: PT.func.isRequired,
244 data: PT.any, /* Should be the actual type of data you expect. */
245 dataTimestamp: PT.number.isRequired,
246 load: PT.func.isRequired,
247 loadingData: PT.bool.isRequired,
248 updateReferenceCounter: PT.func.isRequired,
249};
250
251function mapStateToProps(state) {
252 const s = state.myData;
253 return {
254 data: s.data,
255 dataTimestamp: s.timestamp,
256 loadingData: Boolean(s.loadingOperationId),
257 };
258}
259
260function mapDispatchToProps(dispatch) {
261 const a = actions.myData;
262 return {
263 cleanUp: () => dispatch(a.cleanUp()),
264 load: (...args) => {
265 const operationId = shortId();
266 dispatch(a.loadItemInit(operationId));
267 dispatch(a.loadItemDone(operationId, ...args));
268 },
269 updateReferenceCounter: shift => dispatch(a.updateReferenceCounter(shift)),
270 };
271}
272
273export default redux.connect(mapStateToProps, mapDispatchToProps)(Example);
274```