Redux structure for large apps

After the post that demonstrates the need for external state management, and after the unpublished post comparing Redux MobX & the Flux architecture, I got to the next step, checking how can Redux scale to large applications, since all the examples are on mini-apps, and Redux brings a lot of boilerplate code that can get messy for large applications.

The app is a simple example of a list of objects, and an active object.

Pure React Baseline

The first baseline demonstrating a basic app structure. The first implementation is a standalone React app with no state management tool.

The Item object:

class Item {
    id: number
    title: string

    constructor(title){
        this.id = Math.floor(Math.random()*10000;
        this.title = title; 
    }
}

Main app:

const App = ()=> {
    const [items,setItems] = React.useState<Array<Item>>([new Item('First path')]);
    const [activeItem,setActiveItem] = React.useState<Item|null>(items[0]);

    const addNewPath = () => {
        const newPath = new Item('one more path');
        setItems([...items,newPath]);
        setActiveItem(newPath);
    }

    const resetList = () => {
        setItems([]);
        setActiveItem(null);
    }

    const setMe = async (id)=>{
        setActiveItem(items.find(p => p.id == id));
    }

    return <div>
        <ul>{
            items.map(path => <li key={path.id} onClick={()=>setMe(path.id)}>{path.title}</li>)
        }</ul>
        <button onClick={()=>addNewPath()}>Add New</button>
        <button onClick={()=>resetList()}>Reset</button>
        <ActiveItem item={activeItem} />
    </div>
}

And the ActiveItem component:

type ActiveItemProp = {
    item: Item
}

const ActiveItem:React.FC<ActiveItemProp> = ({item}) => {
    if(item == null) return null
    return  <p>Active: {item.title}</p>
}

Simple Redux implementation:

Changing the data structure from list to state with a normalised data structure:

state = {
    items: {
      123: {'id': 123, 'title': 'bla bla'},
      321: {'id': 321, 'title': 'blo blo'},
    },
    activeItem: 123
}

Create the Redux items:

const initialState = {items:{},activeItem:null}

function itemsReducer(state = initialState, action){
    switch(action.type){
        case 'AddItem':
            const items = {...state.items};
            items[action.payload.item.id] = action.payload.item;
            return {items: items,activeItem: action.payload.item.id};
        case 'ResetList':
            return {items:{},activeItem:null};
        case 'SetActive':
            return {...state, activeItem: action.payload.id}
    }
    return state;
}

const store  = Redux.createStore(itemsReducer);

const mapStore = (state)=>{
    return state;
} 

const App = ReactRedux.connect(mapStore)(AppWithRedux)

And the React modifications:

ReactDOM.render(<ReactRedux.Provider store={store}><App /></ReactRedux.Provider>, document.querySelector('#app'))

const AppWithRedux = ({items,activeItem})=> {

    const addNewPath = () => {
        const newItem = new Item('one more path: '+ Math.floor(Math.random()*10000));
        store.dispatch({type:'AddItem', payload:{item:newItem}})
    }

    const resetList = () => {
        store.dispatch({type:'ResetList'})
    }

    const setMe = async (id)=>{
        store.dispatch({type:'SetActive',payload: {id:id}})
    }

    const itemsList = [];
    Object.entries(items).forEach(a=> itemsList.push(a[1]))

    return <div>
        <ul>{
            itemsList.map(item => <li key={item.id} onClick={()=>setMe(item.id)}>{item.title}</li>)
        }</ul>
        <button onClick={()=>addNewPath()}>Add New</button>
        <button onClick={()=>resetList()}>Reset</button>
        <ActiveItem item={items[activeItem]} />
    </div>
}

Large Redux app structure

#1 split the reducer:

The first thing we will want on a large app is to split the store into different files / sections:

function itemsReducer(state = {}, action){
    switch(action.type){
        case 'AddItem':
            const items = {...state};
            items[action.payload.item.id] = action.payload.item;
            return items;
        case 'ResetList':
            return {};
    }
    return state;
}

function currentStateReducer(state = {activeItem: null}, action){
    switch(action.type){
        case 'SetActive':
            return { activeItem: action.payload.id}
    }
    return state;
}

const appReducer = Redux.combineReducers({
    items: itemsReducer,
    currentState: currentStateReducer
});

const store = Redux.createStore(appReducer);

#2 change the mapper method:

const mapStore = (state)=>{
    return {items:{...state.items},activeItem:state.currentState.activeItem};
} 

#3 Use a well defined actions list:

enum ActionTypes {
    AddItem,
    ResetList,
    SetActive
}

function itemsReducer(state = {}, action){
    switch(action.type){
        case ActionTypes.AddItem:
            const items = {...state};
            items[action.payload.item.id] = action.payload.item;
            return items;
        case ActionTypes.ResetList:
            ...
            ...

const AppWithRedux = ({items,activeItem})=> {
    const addNewPath = () => {
        const newItem = new Item('one more path: '+ Math.floor(Math.random()*10000));
        store.dispatch({type:ActionTypes.AddItem, payload:{item:newItem}})
    }

    const resetList = () => {
        store.dispatch({type:ActionTypes.ResetList})
    }
    ...
    ...

#4 Replace piping with Redux selectors

Split the mapping method into two. And now instead of returning the current item id I can return the actual item

const appStore = (state)=>({...state.items});
const App = ReactRedux.connect(appStore)(AppWithRedux)

const currentStateStore = (state)=>({item:state.items[state.currentState.activeItem]})
const ActiveItem = ReactRedux.connect(currentStateStore)(ActiveItemWithRedux)

And remove the piping from React:

const AppWithRedux = (items)=> {
    ...
    ...
        <button onClick={()=>addNewPath()}>Add New</button>
        <button onClick={()=>resetList()}>Reset</button>
        <ActiveItem />
    </div>
}

const ActiveItemWithRedux:React.FC<ActiveItemProp> = ({item}) => {
    if(item == null) return null
    return  <p>Active: {item.title}</p>
}

#5 Set the action as well defined actions list:

const ItemActions = {
    addNew: newItem => store.dispatch({type:ActionTypes.AddItem, payload:{item:newItem}}),
    reset: () => store.dispatch({type:ActionTypes.ResetList})
}

const CurrentStateActions = {
    setActive : id => store.dispatch({type:ActionTypes.SetActive,payload: {id:id}})
}

const AppWithRedux = ({items,activeItem})=> {
    const addNewPath = () => {
        const newItem = new Item('one more path: '+ Math.floor(Math.random()*10000));
        ItemActions.addNew(newItem);
    }

    const resetList = () => {
        ItemActions.reset();
    }

    const setMe = async (id)=>{
        CurrentStateActions.setActive(id);
    }
    ...
    ...

New we can to some additional stuff, like change different state on the same action:

const ItemActions = {
    addNew: newItem => {
        store.dispatch({type:ActionTypes.AddItem, payload:{item:newItem}});
        store.dispatch({type:ActionTypes.SetActive,payload: {id:newItem.id}})
    },
    ...
    ...

Or add some async tasks:

const ItemActions = {
    loadNew: async (itemId) => {
        const loadedItem = await fetch(somePath + itemId);

        store.dispatch({type:ActionTypes.AddItem, payload:{item:loadedItem}});
        store.dispatch({type:ActionTypes.SetActive,payload: {id:loadedItem.id}})
    },
    ...
    ...

Conclusion

In the beginning, I really didn’t like Redux, with all its boilerplate code. But once you get into React, and pass the small app stage, you see the benefit of well-structured immutable, expectable testable data, I adopted into my toolkit. I still need to figure out some sections that just don’t smell well enough, but it was fun without the endless grazing FE fields to collect additional information.