How to organize Redux state for reusable components?
Asked Answered
E

1

17

TL;DR: In case of a reusable component which has some complicated logic for managing its own state (think: a facebook comment textarea with autocompleter, emoji etc) how does one use store, actions and reducers to manage the state of multiple instances of this component spread across whole website?

Consider the real-world example from the official redux repo. In it we have:

  • a RepoPage, which displays list of users who have starred a particular repo,
  • a UserPage, which displays a list of repos which are starred by particular user
  • a List, which is generic enough that it can display list of users or repos, provided the items and way to renderItem. In particular RepoPage uses User component to display each of users who starred the repo, and UserPage uses a Repo component to display each of starred repos.

Assume that I really want all of the state to be in Redux.

In particular, I want the state of every List on every RepoPage and UserPage to be managed by Redux. This is already taken care of in the example, by a clever three-level deep tree:

  • at the top level the key says what kind of component data is it (in the example it is called store.pagination)
  • then there is a branch for each particular type of context in which the component can be (store.pagination.starredByUser, store.pagination. stargazersByRepo)
  • then there are as many keys as there are unique contexts (store.pagination.starredByUser[login], store.pagination. stargazersByRepo[repo])

I feel that these three levels correspond also to: component type, parent type, parent id.

But, I don't know how to extend this idea, to handle the case in which the List component itself had many children, with a state worth tracking in Redux.

In particular, I want to know how to implement a solution in which:

  • User component remains intact
  • Repo component has a button which toggles its background color
  • the state of each Repo component is managed by Redux

(I'm happy to use some extensions to Redux, which still use reducers, but don't want to go with "just keep it in React local state", for the purpose of this question)

My research so far:

  • it looks like in Elm the Actions (messages) are algebraic data types which can be nested in such a way, that a parent component can unpack an "outer envelope" of the message and deliver a inner action intended for child to the child reducer (updater).
  • since it is a convention in Redux to use a string as the type of action, a natural translation of the above idea is to use prefixing, and this seems to be what prism (foremly known as redux-elm) does: the action.type is comprised of substrings which tell the path through components' tree. OTOH in this comment the prism author tomkis explains that the most important part of Elm Architecture that Redux is missing is composition of actions
  • the two above approaches seem to be expanded versions of approaches described in Reusing Reducer Logic
  • I haven't fully grasped how redux-fly works internally, but it seems to use the payload, not the action.type to identify a component instance by its mounting path in the store which also corresponds to a path in the components tree because of the way it is constructed manually by components
  • WinAPI, which to me seems quite similar to Redux if you squint, uses unique hWnd identifier for each control, which makes it super easy to check if action was intended for you, and decide where should be your state in the store.
  • The above idea could probably lead to something described in Documentation suggestion/discussion: Reusing Reducer Logic where each type of component has its own flat subtree indexed by unique id.
  • Another idea descibed in the linked thread linked above is to write a reducer for a particular type of component once, and then let the reducer for the parent component call it (which also means, that the parent is reponsible to decide where in the store the state of the child is located - again, that seems similar to Elm Architecture to me)
  • A very interesting discussion More on reusability for custom components in which details of a proposal vary similar to the one above is presented
  • in particular above discussion contains a proposition by user nav, to organize the store tree recursively in such a way, that a state of a component is subtree in two kinds of branches: one for private stuff, and the other for "tables" of child components, where each class of child component has its own "table", and each instance of child has a unique key in that table, where its state is recursively stored. The unique keys which give access to these children are stored in the "private" section. This is really similar to how I imagine WinAPI :)
  • another elm-inspired proposition by user sompylasar from the same thread is to use actions which contain actions for children as a payload in a "matrioshka" style, which in my opinion mimick how algebraic types constructors are nested in Elm
  • redux-subspace was recommended in discussion about Global Actions for prism, as a library which is both Elm-inspired and lets you have global actions.
Erratum answered 28/7, 2017 at 8:8 Comment(1)
Creator of redux-subspace here. I have recently taken a stab at the real-world example and how it might look with isolated components. Take a look in the repo or the sandbox if you're interested.Dibromide
S
5

I will try to explain one of idea which is inspired by Elm lang and has been ported to Typescript:

Let's say we have very simple component with the following state

interface ComponentState {
   text: string
}

Component can be reduced with the following 2 actions.

interface SetAction {
    type: 'SET_VALUE', payload: string
}

interface ResetAction {
    type: 'RESET_VALUE'
}

Type union for those 2 actions (Please look at Discriminated Unions of Typescript):

type ComponentAction = SetAction | ResetAction;

Reducer for this should have thw following signature:

function componentReducer(state: ComponentState, action: ComponentAction): ComponentState {
    // code
}

Now to "embed" this simple component in a larger component we need to encapsulate data model in parent component:

interface ParentComponentState {
    instance1: ComponentState,
    instance2: ComponentState,
}

Because action types in redux need to be globally unique we cannot dispatch single actions for Component instances, because it will be handled by both instances. One of the ideas is to wrap actions of single components into parent action with the following technique:

interface Instance1ParentAction {
    type: 'INSTNACE_1_PARENT',
    payload: ComponentAction,
}

interface Instance2ParentAction {
    type: 'INSTNACE_2_PARENT',
    payload: ComponentAction,
}

Parent action union will have the following signature:

type ParentComponentAction = Instance1ParentAction | Instance2ParentAction;

And the most important thing of this technique - parent reducer:

function parentComponentReducer(state: ParentComponentState, action: ParentComponentAction): ParentComponentState {
    switch (action.type) {
        case 'INSTNACE_1_PARENT':
            return {
                ...state,
                // using component reducer
                instance1: componentReducer(state.instance1, action.payload),
            };
        //
    }
}

Using Discriminated Unions additionally gives type safety for parent and child reducers.

Satiated answered 25/9, 2017 at 20:12 Comment(1)
Hi Tomasz as you had mentioned I will try to explain one of idea which is inspired by Elm lang and has been ported to Typescript: can you point me to a repo for the same . i am having a similar use case where i want the common component to be used across several components with their own individual stateBushweller

© 2022 - 2024 — McMap. All rights reserved.