Namespacing Actions for Redux – Kickstarter Engineering


Constraints

In this way, the first level of research allowed us to flesh out what requirements a successful solution would support.

We needed a way to namespace actions that:

  1. Did not involve using local state
  2. Worked with combineReducers
  3. Could be applied programmatically
  4. Had a low–API surface area, which is to say, did not ask to much of others using the resultant component

The RFC

With these constraints in mind, we identified three solutions: nested reducers, higher-order reducers, and the module pattern, and wrote up an RFC for the front-end team to consider.

Nested Reducers

The first solution we tried was to use nested reducers, which was inspired by the Elm architecture. At the time of the RFC, it had been implemented in a few locations in our codebase.

As this example makes plain, however, this approach contravened the goal of simple instantiation. While it’s likely possible to write generator functions in order to avoid manual instantiation, the path to that is definitely not straightforward. Many members of our team also felt this approach seemed unneccesarily complex.

Higher-Order Reducers

Focusing on reducers that could be more straightforwardly be generated for namespacing, brought us to higher-order reducers. This approach centers on a reducer generator function that returns a reducer that only executes when called with a named action.

It would be paired with a higher-order action creator:

This solution hit the programmatic constraint pretty well, but broke down amid the low–API surface area desires.

The Module Pattern

In the film Hidden Figures, there is a part where the mathematicians and engineers are struggling to figure out the best way to compute a trajectory and Katherine Johnson comes up with the solution by going back to “old” math. Though this is definitely a silly way to put it and almost certainly came from the pen of a screenwriter and not the mouth of a mathematician, it also aptly describes the final pattern we considered — a relic from old Javascript.

The module pattern was a popular way of namespacing functions back in the “old” days of ES5 and worked by creating an immediately-invoked function expression (IIFE), which would use the power of closures to create functions that would not clash with one another.

For instance, with this example counter, the variable numcan be operated on by the functions in the returned object, but it will not clash in case the same name is used elsewhere in the code.

Applying this to our problem brought us to this suggestion:

which was very promising. It:

  1. Did not involve using local state
  2. Worked with combineReducers
  3. Could be applied programmatically
  4. Had a low–API surface area: it required only state and dispatch to be instantiated, like the non-namespaced version.

The obvious downsides to this approach were the need to wrap the entire component in the scoping function and the reliance on string interpolation.

The latter became more than a downside when we tried to apply the pattern within our concurrent TypeScript experiment. Namespacing the action strings interfered with the team’s ability to declare action types as a union type on the action’s string.

But we forged ahead. And we succeeded!

Interacting with one box only affects that box. Try it on CodePen.

Our Solution: Where We Landed

In the end, we settled on a solution that combined elements from higher-order reducers and the module pattern. Instead of using string interpolation, we added a namespace value to our actions and applied the higher-order reducer pattern. In terms of the module pattern from above, the change results in a module that looks like this:

In order to mitigate the other drawback — being forced to work inside the closure—we added a set of utility functions to add the namespaces in.

This way, a developer could implement the namespacing elements in her module as she saw fit. The only requirements were that reusable components should provide a namespacing function that could be called with a namespace string but otherwise would default to a uuid. This function would return:

  • namespaced actions with the structure { type, namespace, ...otherPayload }
  • a namespaced wrapper component that can be instantiated with state, dispatch, and optional configuration parameters
  • a namespaced reducer that handles passing the namespace through all actions, even those returned by redux-loop
  • the namespace itself (useful if users want to autogenerate namespaces but refer to them)

In addition to this function, the component is expected to provide an initial state with appropriate defaults/blank state, suitable for being folded into the top-level state.

The utilities also include an all-in-one module namespacing function that allows a developer to abstract namespacing to the factory functions:

It is worth noting that the namespacing function provides for a number of arguments, not just the namespace itself, but also the component, reducer and actions. This was put in place to address cases where components might need to be wrapped before being passed to the namespacer, for instance a themed component. In that case, a component might be a curried function that takes its theme as the first argument and then returns the wrapper component. To namespace this, the themed component could be passed as part of the options, supplanting the usual version.

Challenges & Successes

The primary challenge we have encountered with this approach so far has been keeping actions namespaced as we pass them through reducers. Whenever the result of a dispatched action is to further dispatch other actions, those actions need to maintain their namespace.

We’ve chosen to assure this by binding actions to reducers, usually through partial application on initialization:

In a one-step initialization, as above, this works well. We have run into a few instances of complex composition, however, where the binding has gotten lost and caused errors.

The other drawback to this approach is that instead of relying on shared scope for actions returned from promises, we need to pass the namespaced version as an argument.

The verbosity is a reasonable trade-off here, though, since the handler code needs only to be written once, but components will be instantiated multiple times.

And despite the kinks, we have found this approach to work successfully for most cases, including compound components that wrap a base reusable component with greater functionality, like a base uploader that can be wrapped to become an image uploader or a video uploader or a custom select that can have async option fetching also added in.

One larger concern with this approach is that at some point there may be too many reducers and that will result in a performance hit. There are a few ways to approach mitigation here, from libraries that help single out subreducers to ignore to reconstituting reducers as hash-maps instead of case statements. But we don’t need to solve problems before we hit them, so we’ll be sticking with our adapted modules for now.



Source link