25
loading...
This website collects cookies to deliver better user experience
const state = {
editFeaturesModal: {
isOpen: false,
features: [{ id: 'some-feature', derp: 123 }], // from API
selected: ['some-feature'],
},
removeFeaturesModal: {
isOpen: true,
features: [{ id: 'some-feature', derp: 123 }], // also from API, duplicate!
removed: ['some-feature'],
},
};
features
returned by a /features
API route should be stored in the global state with IDs. State scoped to a particular experience, like editFeaturesModal
that keeps track of features to appear in a user's dashboard, should reference the "selected" features
by an ID, not by storing the entire feature
object://bad
const state = {
editFeatures: {
isOpen: true,
selected: [{ id: 'some-feature', derp: 123 }], // copies a `feature` object
},
features: [{ id: 'some-feature', derp: 123 }],
};
// better
const state = {
editFeatures: {
isOpen: true,
selected: ['some-feature'], // "points" to a `feature` object instead of copying it
},
features: [{ id: 'some-feature', derp: 123 }],
};
// SomeComponent.js
function SomeComponent() {
const dispatch = useDispatch();
useEffect(() => {
async function fetchData() {
const resp = await fetch(...);
const { users , ...rest } = await resp.json();
const result = {
authenticatedUsers: {
....users,
isEmpty: users.length > 0,
},
options: { ...rest },
};
dispatch(fetchUsers(result));
}
fetchData();
}, [dispatch]);
}
// actions.js
function fetchUsers({ authenticatedUsers, options }) {
dispatch({ type: 'FETCH_USERS', users: authenticatedUsers, isCalculated: authenticatedUsers.isCalculated, options });
}
// reducer.js
case 'FETCH_USERS': {
return {
...state,
users: {
authenticated: {
...action.payload.users,
isSet: isCalculated,
....action.payload.options,
},
},
};
}
useEffect
hook, the action creator, and the reducer. Yuck!const state = {
editFeatureModal: {
features: [{ id: 'some-feature', derp: 123 }],
},
isShowingAnotherModal: true,
users: [{ id: 'some-user', derp: 123 }],
};
users
could contain the response of an API, isShowingAnotherModal
refers to state controlling a modal's visibility, and editFeatureModal
refers to state for a specific modal workflow, but it also contains state that could be from an API response.const slice = {
editFeatureModal: {
features: [{ id: 'some-feature', derp: 123 }],
},
isShowingAnotherModal: true,
users: [{ id: 'some-user', derp: 123 }],
};
const state = {
app: {
authenticatedUser: {
email: '[email protected]',
},
},
experiences: {
editFeatures: {
isOpen: true,
selected: ['some-feature'],
},
},
api: {
features: [{ id: 'some-feature', derp: 123 }],
},
};
app
, experiences
, and api
as the top-level properties. Or, perhaps you want to make one of the types the implicit default:const state = {
app: {
authenticatedUser: {
email: '[email protected]',
},
},
api: {
features: [{ id: 'some-feature', derp: 123 }],
},
// "experiences" is the implicit default type in the state
editFeatures: {
isOpen: true,
selected: ['some-feature'],
},
};
app
and api
is one without a difference.app
and api
in my example) should store entire datasets (i.e. authenticatedUser
and features
).editFeatures
experience (a modal for editing the features of a user's dashboard), needs to keep track of features that a user wants to select/enable for their dashboard, then it should only store an id
that "points" to an object in the api.features
list:const state = {
experiences: {
editFeatures: {
isOpen: true,
selected: ['some-feature'], // points to a `api.features` object
},
},
api: {
features: [{ id: 'some-feature', derp: 123 }],
},
};
api.features
object as the "table" and the experiences.editFeatures.selected
are foreign keys to the table when making an analogy with databases.Data with IDs, nesting, or relationships should generally be stored in a “normalized” fashion: each object should be stored once, keyed by ID, and other objects that reference it should only store the ID rather than a copy of the entire object. It may help to think of parts of your store as a database, with individual “tables” per item type.
features
from the API.features
under two "experience" properties:const state = {
editFeaturesModal: {
isOpen: false,
features: [{ id: 'some-feature', derp: 123 }],
isFeaturesLoading: false,
selected: ['some-feature'],
},
removeFeaturesModal: {
isOpen: true,
features: [{ id: 'some-feature', derp: 123 }],
isFeaturesLoading: false,
removed: ['some-feature'],
},
};
/features
route, or you will have to awkwardly reference another experience without a clear establishment of a "source of truth" for the features list.api.features
property and the experience.editFeatures
and experience.removeFeatures
properties, an EditFeatures
or RemoveFeatures
component can avoid an API request if api.features
is not empty, and both components can pick the api.features
property without confusingly referencing a property in the state coupled to another experience (i.e. EditFeatures
referincing removeFeaturesModal.features
).features
on each modal to avoid stale data, the latter benefit still remains.api
, experiences
, and app
. Arguably, we could condense api
and app
into one, maybe calling it data
.data
and experiences
are separated, there is no explicit way to associate between a experience and the data it references.data
and experiences
by "domains."const state = {
shoppingCart: {
data: {
upsells: [{ id: 'some-upsell', derp: 123 }, { id: 'another-upsell', herp: 456 }],
},
editCartModal: {
isOpen: false,
upsells: ['some-upsell'],
},
cart: {
upsells: ['some-upsell', 'another-upsell'],
},
},
};
/* tree */
src/
store.js
/shopping-cart
/modals
/cart
slice.js
/* slice */
const slice = {
shoppingCart: {
data: {
upsells: [{ id: 'some-upsell', derp: 123 }, { id: 'another-upsell', herp: 456 }],
},
editCartModal: {
isOpen: false,
upsells: ['some-upsell'],
},
cart: {
upsells: ['some-upsell', 'another-upsell'],
},
},
};
/* store */
const store = combineSlices(shoppingCart, ...);
isOpen
flags by scoping a component to a route in the router. You can then toggle the component's visibility by toggling the route.