What happened?
Recently I lost quite a few hours debugging an issue in a thunk that looked more or less like:
const fetchData = (id) => (dispatch, getState, api) => {
if (Object.keys(getState()).length === 0) {
api.get(id).then((data) => dispatch({ type: 'FETCHED_DATA', data })
}
}
Essentially, the idea here was to only fetch the data only if the state didn't already have any keys.
The reducer looked something like:
const reducer = (state = {}, action) => {
if (action.type === 'FETCHED_DATA') {
return { ...state, ...action.data }
}
return state
}
This worked fine when developing and testing the component, but would never attempt to fetch the data once it was integrated into a parent component.
Eventually I tracked it back to the use of wormholes in the root application. The wormholes added additional keys to the state, so when checking the length of the keys, it was never empty.
What should have happened?
In the above scenario, I would have preferred if the state was unchanged, particularly as this component did not care about any of the wormholed values. That way, the component would not need to be modified to work within the root app.
Any Ideas?
The issue I see here is the way wormholes get globally applied to all subspaces. This can lead to difficult to find bugs in child components that had no control over what the wormholes, if any, are being applied to their state. The scenario above is an example of this.
I propose changing the wormhole API to be an opt in addition to the components state. In the new API, components would nominate which wormholes, if any, they want to receive, and if no wormholes are nominated the components state is unchanged.
I'm thinking that this could work similarly to React's Context feature where higher in the tree, a component can add values into context, and descendants of the components can opt into received one or more of those values. There is also potential here to improve the developer experience here by providing dev time warnings if the requested wormhole values are not provided by the parent.
Translating this concept to subspaces, the wormhole
middleware would be the point higher in the tree to add the values. Currently, multiple wormholes are added a seperate calls to the wormhole
middleware. In this proposal the api will change to an object of key to wormhole selector, i.e.:
const store = createStore(reducer, applyMiddleware(
wormhole((state) => state.value1, 'key1'),
wormhole((state) => state.value2, 'key2'),
wormhole((state) => state.value3, 'key3')
))
would become:
const store = createStore(reducer, applyMiddleware(
wormholes({
key1: wormhole((state) => state.value1),
key2: wormhole((state) => state.value2),
key3: wormhole((state) => state.value3)
})
))
This is arguably a nicer API than the existing one if there are lots of wormholes, but it does remove the ability to use the selector shorthand that has been adopted in many of the subspace feature (i.e. wormhole((state) => state.value, 'value')
can be shortened to just wormhole('value')
). I'd like to investigate ways to maintain backwards compatibility with the existing API on this end, but my feeling is that it won't be possible if we want to provide warnings when requested keys are missing.
On the sub-component side placeholder values would be added to it's state to select the values the component wants to be included from the wormholes.
This could be done as a higher-order reducer:
const reducer = combineReducers({ reducer1, reducer2)
const enhancedReducer = withWormholes('key1', 'key3')(reducer)
or as a reducer to combine with others:
const reducer = combineReducers({
reducer1,
reducer2,
wormholes: wormholesReducer('key1', 'key3')
)
There are advantages and disadvantages to both approaches, so I would be keen to hear how you would prefer to use it.
When it comes to using the subspaces, the wormhole
middleware would inspect the state of the subspace and replace any of the placeholder values with the actual values from the wormhole selectors (and warn about any that can't be found). There is also potential here to add shape validation (e.g. prop-types
) to the wormholed values to ensure the parent is providing them in a usable format for the child, again improving the developer experience.
Pros
- Declarative
- Component state is closer to runtime expectations
- Better developer experience
Cons
- Inspecting the state in the dev tools would display the placeholder values in their raw format
- It is (probably) not backwards compatible with the current implementation
One of the biggest differences I see in philosophy between the current implementation and explicit nature of this proposal is that currently, the components don't have to care where the extra values in their state come from. They can be from wormholes, added when mapping the component state in a SubspaceProvider
or any other kind of magic you can pull off the get the values there when getState
is called, but with the new approach, they can only come from a wormhole. There is a whole series of questions this philosophical change brings in, but I don't have time to go into those now. Perhaps I'll add a following post to ask those soon.
Anyway, how does all this sound? Let me know what you think, ask any questions you want, suggest changes to the proposal, or propose your own changes.