1 | # Item (Redux Template)
|
2 |
|
3 | The 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
|
6 | is asyncroneously fetched from some API, or generated some other way, and should
|
7 | be updated periodically. **item** actions and reducer provided by our library
|
8 | implement a good way to handle this.
|
9 |
|
10 | ### Content
|
11 | - [Overview](#overview)
|
12 | - [Tutorial](#tutorial)
|
13 |
|
14 | ### Overview
|
15 |
|
16 | Say you need to load (generate) and keep in Redux store some piece of `DATA`,
|
17 | which can be of any JS type (Boolean, Number, String, Object, etc.). To handle
|
18 | it 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 |
|
29 | Our 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 |
|
62 | Say, you want to add `myData` segment to your Redux store, using **item**
|
63 | actions / reducers. You want multiple ReactJS containers rely on this segment,
|
64 | and you want to properly clean-up stale data when no container is using them.
|
65 |
|
66 | For `myData` actions module you should do:
|
67 | ```js
|
68 | // src/shared/actions/myData.js
|
69 |
|
70 | import { actions, redux } from 'topcoder-react-utils';
|
71 |
|
72 | const 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.
|
77 | async 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 |
|
84 | export 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 |
|
95 | Now, you create `myData` reducer the following way (the layout of module follows
|
96 | our standard practice):
|
97 | ```js
|
98 | // src/shared/reducers/myData.js
|
99 |
|
100 | import actions from 'actions/myData';
|
101 | import { actions as truActions, reducers, redux } from 'topcoder-react-utils';
|
102 |
|
103 | const itemActions = truActions.item;
|
104 | const itemReducer = reducers.item;
|
105 |
|
106 | /* Custom reducer function to handle possible errors in a custom way. */
|
107 | function 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 | */
|
127 | function 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 | */
|
145 | export 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. */
|
155 | export default create();
|
156 | ```
|
157 |
|
158 | Now, provided that you have embed this reducer into your root reducer in
|
159 | the regular way, you can follow the following pattern for a container that
|
160 | relies on these data:
|
161 | ```js
|
162 | // src/shared/containers/example.js
|
163 |
|
164 | import actions from 'actions/myData';
|
165 | import Example from 'components/Example';
|
166 | import PT from 'prop-types';
|
167 | import React from 'react';
|
168 | import shortId from 'shortid';
|
169 | import { redux } from 'topcoder-react-utils';
|
170 |
|
171 | const MAX_AGE = 5 * 60 * 1000; // 5 min.
|
172 | const RELOAD_AGE = 1 * 60 * 1000; // 1 min.
|
173 |
|
174 | class 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 |
|
238 | ExampleContainer.defaultProps = {
|
239 | data: null,
|
240 | };
|
241 |
|
242 | ExampleContainer.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 |
|
251 | function 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 |
|
260 | function 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 |
|
273 | export default redux.connect(mapStateToProps, mapDispatchToProps)(Example);
|
274 | ```
|