«PoC» React+Redux the Extensible Way#

With this article I try to create an extensible version of a Web Application based on React and Redux. Completed

../../_images/ui.png

To do so I create first a small example with a few requirements. Those requirements shall be satisfied step by step according the release roadmap.

The sources are also available in the repository your-app-name. The commit history reflects rougly the release roadmap.

Requirements#

Req_01:

The user shall be able to interact with the app by entering values and getting calculated values derived from their inputs.

Req_02:

The app shall have a button. When the user presses this button a value is incremented. The button label shows that value. Initial value is 19.

Req_03:

The app shall have a second button. When pressed it retrieves a value from an API call and adds it to its own value which is displayed in the button label. Initial state of the own variable is 76.

Req_04:

The app shall display the sum of the values of ComponentA und ComponentB.

Req_05:

The app shall provide a button to reset the variables to their initial ones.

Release Roadmap#

Releases and the Requirements to satisfy:

    Rel_1           Rel_2       Rel_3       Rel_4
---|---------------|-----------|-----------|--------->

    Req_01          Req_01      Req_01      Req_01
    Req_02          Req_02      Req_02      Req_02
                    Req_03      Req_03      Req_03
                                Req_04      Req_04
                                            Req_05

Architectural Design#

Concept#

The application is built using a modular architecture that allows for easy extension and maintenance. Each component manages its own state and reducers, which are dynamically registered to the store. This approach ensures that the application can scale easily as new features are added.

../../_images/swad_concept.svg

Components contribute slices to the store#

Building Block View#

We emphasize modularity and extensibility. Each component manages its own state and reducers, which are dynamically registered to the store. This allows the application to scale easily as new features are added.

../../_images/bd_swad_building_blocks.svg

Building Block View#

Todo

Add description of interfaces between blocks

Runtime View#

Todo

Add runtime view

Sequence Diagrams for the “good” and “bad” scenarios

“Good” scenarios:

  • «event» Frontend (re)-load

  • «event» Backend start

  • «event» click on 1st button

  • «event» click on 2nd button

“Bad” scenarios:

  • «event» click on 2nd button while API is down

Scenario «event» Frontend (re)-load#

@startuml
actor User
box Frontend
participant App
end box

User --> App : load
@enduml

Frontend (re)-load#

Todo

Finish Scenario «event» Frontend (re)-load

Design Alternatives Considered#

  1. Single Store File: Initially, we considered managing all reducers in a single store/index.js file. However, this approach is not scalable as it requires modifying the store file every time a new reducer is added, leading to tight coupling and reduced maintainability.

  2. Manual Reducer Registration: Another approach was to manually register reducers in the store/index.js file. This was rejected because it hinders extensibility. Instead, we opted for a dynamic registration mechanism that allows each component to register its own reducer.

  3. Hardcoded Reducers: Hardcoding reducers in the main store configuration was also considered but discarded due to the lack of flexibility. Dynamic registration provides better modularity and allows for lazy loading of reducers.

Traceability Reqs to Architectural Elements#

Req_01  is satisfied by   App, Store
Req_02  is satisfied by   ComponentA
Req_03  is satisfied by   ComponentB, Backend
Req_04  is satisfied by   ComponentC
Req_05  is satisfied by   ComponentD
Component            Req01  Req02  Req03  Req04  Req05
=========================================================
Core ----------------------------------------------------
App                   X     (x)    (x)    (X)    (x)
Store                 X
Backend                             X
Extensions ----------------------------------------------
    ComponentA        X      X
    ComponentB        X             X
    ComponentC        X                    X
    ComponentD        X                           X
----------------------------------------------------------


|  X   : Component is responsible to satisfy requirement.
| (x)  : Component needs minor modifications to satisfy requirement

Detailed Design & Construction#

Directory structure#

-- FRONTEND ---------------------------
/src
    App.js
    index.js
    /store
        reducerManager.js
        store.js
    /components
        /ComponentA
            index.js
            slice.js
        /ComponentB
            index.js
            slice.js
        /ComponentC
            index.js
            slice.js
        /ComponentD
            index.js
            slice.js
---------------------------------------

-- BACKEND ----------------------------
/backend
    __init__.py
    api.py
    app.py
    models.py
---------------------------------------

External Dependencies#

Create Skeleton of React Application (creates external dependencies):

npx create-react-app your-app-name
cd your-app-name

Install packages (adds additional external dependencies):

npm install \
    @reduxjs/toolkit \
    react-redux \
    redux-thunk \
    redux-logger \

«component» App#

src/index.js#
 1import React from 'react';
 2import ReactDOM from 'react-dom/client';
 3import App from './App';
 4
 5const root = ReactDOM.createRoot(document.getElementById('root'));
 6root.render(
 7  <React.StrictMode>
 8    <App />
 9  </React.StrictMode>
10);
src/App.js#
 1import React from 'react';
 2import { Provider } from 'react-redux';
 3import store from './store';
 4
 5import ComponentA from './components/ComponentA'
 6import ComponentB from './components/ComponentB'
 7import ComponentC from './components/ComponentC'
 8import ComponentD from './components/ComponentD'
 9
10const App = () => {
11    return (
12        <Provider store={store}>
13        <ComponentA/>
14        <ComponentB/>
15        <ComponentC/>
16        <ComponentD/>
17        </Provider>
18    );
19};
20
21export default App;

«component» Store#

src/store/index.js#
 1import { configureStore } from '@reduxjs/toolkit';
 2import { thunk } from 'redux-thunk';
 3import logger from 'redux-logger';
 4import { createReducerManager } from './reducerManager';
 5
 6const registerReducer = (key, reducer) => {
 7    store.reducerManager.add(key, reducer);
 8    store.replaceReducer(store.reducerManager.reduce);
 9  };
10
11export { registerReducer };
12
13const initialReducers = {};
14
15const reducerManager = createReducerManager(initialReducers);
16
17const store = configureStore({
18  reducer: reducerManager.reduce,
19  middleware: (getDefaultMiddleware) =>
20    getDefaultMiddleware().concat(thunk, logger),
21});
22
23store.reducerManager = reducerManager;
24
25export default store;
src/store/reducerManager.js#
 1import { combineReducers } from '@reduxjs/toolkit';
 2
 3export const createReducerManager = (initialReducers) => {
 4  const reducers = { ...initialReducers };
 5
 6  let combinedReducer = combineReducers(reducers);
 7
 8  return {
 9    getReducerMap: () => reducers,
10    reduce: (state, action) => combinedReducer(state, action),
11    add: (key, reducer) => {
12      if (!key || reducers[key]) {
13        return;
14      }
15
16      reducers[key] = reducer;
17      combinedReducer = combineReducers(reducers);
18    },
19    remove: (key) => {
20      if (!key || !reducers[key]) {
21        return;
22      }
23
24      delete reducers[key];
25      combinedReducer = combineReducers(reducers);
26    },
27  };
28};

Backend#

Create Skeleton of Python/FastAPI Application:

poetry init \
--name your-app-name \
--description "Backend of The Extensible App" \
--author "Calidus Callidus <calidus@callidus.it>" \
--python "^3.8"
--dependency fastapi \
--dependency uvicorn \

File backend/__init__.py is empty.

«Component» App#

backend/app.py#
 1from fastapi import FastAPI
 2from fastapi.middleware.cors import CORSMiddleware
 3from backend.api import router as api_router
 4
 5app = FastAPI()
 6
 7origins = [
 8    "http://localhost:3000",
 9    "http://127.0.0.1:3000",
10    # Add other origins as needed
11]
12
13app.add_middleware(
14    CORSMiddleware,
15    allow_origins=origins,
16    allow_credentials=True,
17    allow_methods=["*"],
18    allow_headers=["*"],
19)
20
21app.include_router(api_router)
22
23if __name__ == "__main__":
24    import uvicorn
25    uvicorn.run("backend.app:app", host="0.0.0.0", port=8000, reload=True)

«component» Models#

backend/models.py#
1from pydantic import BaseModel
2
3class ValueResponse(BaseModel):
4    value: int

«Component» API#

backend/api.py#
1from fastapi import APIRouter
2from backend.models import ValueResponse
3
4router = APIRouter()
5
6@router.get("/value", response_model=ValueResponse)
7async def get_value():
8    return ValueResponse(value=42)

Extensions#

Each extension manages its own state and reducers, which are dynamically registered to the store.

«component» ComponentA#

src/components/ComponentA/index.js#
 1import React from 'react';
 2import { useSelector, useDispatch } from 'react-redux';
 3import { incrementA } from './slice';
 4
 5const ComponentA = () => {
 6  const valueA = useSelector(state => state.ComponentA.valueA);
 7  const dispatch = useDispatch();
 8
 9  const incrementValueA = () => dispatch(incrementA());
10
11  return (
12     <button onClick={incrementValueA}>
13      Increment Value A: {valueA}
14      </button>
15  );
16};
17
18export default ComponentA;
src/components/ComponentA/slice.js#
 1import { createSlice } from '@reduxjs/toolkit';
 2import { registerReducer } from '../../store';
 3
 4const initialState = {
 5  valueA: 19,
 6};
 7
 8const componentASlice = createSlice({
 9  name: 'ComponentA',
10  initialState,
11  reducers: {
12    incrementA(state) {
13      state.valueA += 1;
14    },
15    resetA(state) {
16      state.valueA = initialState.valueA;
17    },
18  },
19});
20
21export const { incrementA, resetA } = componentASlice.actions;
22
23// Register the reducer when this module is loaded
24registerReducer('ComponentA', componentASlice.reducer);
25
26export default componentASlice.reducer;

«component» ComponentB#

src/components/ComponentB/index.js#
 1import React from 'react';
 2import { useSelector, useDispatch } from 'react-redux';
 3import { incrementB } from './slice';
 4
 5const ComponentB = () => {
 6  const valueB = useSelector(state => state.ComponentB.valueB);
 7  const dispatch = useDispatch();
 8
 9  const fetchValueAndUpdateB = async () => {
10    const response = await fetch(`${window.location.origin.replace(/:\d+$/, '')}:8000/value`); // Use the current origin
11    const data = await response.json();
12    dispatch(incrementB(data.value));
13  };
14
15  return (
16    <button onClick={fetchValueAndUpdateB}>
17      Increment Value B: {valueB}
18    </button>
19  );
20};
21
22export default ComponentB;
src/components/ComponentB/slice.js#
 1import { createSlice } from '@reduxjs/toolkit';
 2import { registerReducer } from '../../store';
 3
 4const initialState = {
 5  valueB: 76,
 6};
 7
 8const componentBSlice = createSlice({
 9  name: 'ComponentB',
10  initialState,
11  reducers: {
12    incrementB(state, action) {
13      state.valueB += action.payload;
14    },
15    resetB(state) {
16      state.valueB = initialState.valueB;
17    },
18  },
19});
20
21export const { incrementB, resetB } = componentBSlice.actions;
22
23// Register the reducer when this module is loaded
24registerReducer('ComponentB', componentBSlice.reducer);
25
26export default componentBSlice.reducer;

«component» ComponentC#

src/components/ComponentC/index.js#
 1import React from 'react';
 2import { useSelector } from 'react-redux';
 3
 4const ComponentC = () => {
 5  const valueA = useSelector(state => state.ComponentA.valueA);
 6  const valueB = useSelector(state => state.ComponentB.valueB);
 7
 8  return (
 9    <div>
10      Sum of Values: {valueA + valueB}
11    </div>
12  );
13};
14
15export default ComponentC;
src/components/ComponentC/slice.js#
 1import { createSlice } from '@reduxjs/toolkit';
 2import { registerReducer } from '../../store';
 3
 4const initialState = {};
 5
 6const componentCSlice = createSlice({
 7  name: 'componentC',
 8  initialState,
 9  reducers: {},
10});
11
12registerReducer('componentC', componentCSlice.reducer);
13
14export default componentCSlice.reducer;

«component» ComponentD#

src/components/ComponentD/index.js#
 1import React from 'react';
 2import { useDispatch } from 'react-redux';
 3import { resetA } from '../ComponentA/slice';
 4import { resetB } from '../ComponentB/slice';
 5
 6const ComponentD = () => {
 7  const dispatch = useDispatch();
 8
 9  const resetValues = () => {
10    dispatch(resetA());
11    dispatch(resetB());
12  };
13
14  return (
15    <button onClick={resetValues}>
16      Reset Values
17    </button>
18  );
19};
20
21export default ComponentD;
src/components/ComponentD/slice.js#
 1import { createSlice } from '@reduxjs/toolkit';
 2import { registerReducer } from '../../store';
 3
 4const initialState = {};
 5
 6const componentDSlice = createSlice({
 7  name: 'ComponentD',
 8  initialState,
 9  reducers: {},
10});
11
12registerReducer('componentD', componentDSlice.reducer);
13
14export default componentDSlice.reducer;

User Guide#

Application Start#

Start your React application:

# Start Backend
poetry run python -m backend.app

# Start Frontend
npm start

# Ready to go!

Usage#

Todo

Add short description on usage

../../_images/ui.png

Conclusion#

Todo

Add conclusion