ncthbrt / react-use-elmish

A React hook for unified purely-functional state and effect management
MIT License
59 stars 4 forks source link

Reducer Composition #6

Closed narthollis closed 4 years ago

narthollis commented 4 years ago

I really like what I am seeing with this library - it looks like it sould produce some quite neat code for handling model/application state and side effects all in one place.

Comming from Redux land the one thing that looks to be missing to me is reducer composition. In Redux you have combineReducers which take slices out a part of the store to be handle by a particular reducer.

eg

import { userReducer } from './users';
import { blogsReducer } from './blogs';

export const reducer = combineReducers({
    user: userReducer,
    blogs: blogsReducer,
});

this would give reducer that handled a store that looked like:

{
    "user": {...},
    "blogs": {...},
}

I am interested in what the best way to acheive something similar with react-use-elmish would be.

(note: while redux + combineReducers says every reducer gets called for every action, I do not nesscarly see this a desirable behavior)

ncthbrt commented 4 years ago

Hi @narthollis Sorry I didn't see this issue till now. My github inbox gets a bit overwhelming sometimes.

Hope you came right in the end. But my general approach would be to use simple function composition.

You could of course achieve a very similar result to redux, by building a small utility function that'd iterate over all subreducers, but I personally feel that that may be in fact an antipattern.

I'd rather compose using actions than by state. For example having a map that stores a set of possible handlers of actions:


function createReducer<State, Action extends {type: string}>(handlers: { [ActionType in Action['type']]:  (state: State, action: Action & { type: ActionType }) => StateEffectPair<State,Action> }) {
    return function reducer(prev:State, action: Action) : StateEffectPair<State,Action> {
        return handlers[action.type](prev,action);
    };
};

const applicationReducer = createReducer({ 
   ...routerHandlers,
   ...blogHandlers,
   LOGOUT: (prev, action) => [{...prev, state: 'logging-out'}, logoutEffect(prev, action)]
});
narthollis commented 4 years ago

This is quite useful thanks!

I have to say that life, work, etc. all caught up on my side I have not had a chance to start working with this yet - but I am quite keen on it. Thanks again for the reply

narthollis commented 4 years ago

I have finally had the time and opportunity to use this library in a project, and I quite like the options it provides!

The circumstance I am working with is a fairly simple single nested reducer setup, so I have gone with a basic approach of just invoking the nested reduce when the action cases for the parent are exhausted.

            default: {
                const [nextSubState, nextEffects] = reducer(state.state, action);
                return [nextSubState !== state.state ? { ...state, state: nextSubState } : state, nextEffects];
            }

Thanks for the library - it really is very easy to work with and proves quite followable workflows.