JavaScript in Plain English

New JavaScript and Web Development content every day. Follow to join our 3.5M+ monthly readers.

Follow publication

Flux Architecture Demystified: Redux and NGRX Aren’t That Hard

--

On Both Redux and NGRX introduction pages ( the flux libraries for React and Angular respectively ) there is a section discussing how you should be careful when deciding whether to use or not this architecture which makes developers fearful of using the libraries and even worse, making them believe in some cases that they cannot understand the how these libraries work behind the scene.

My goal in this article is to show how simple this architecture can be, and how in less than 40 lines of code we can make such an architecture work.

First, let’s have a simple renderer function, that will update the dom when my value changes ( NGRX does it with the help of RXJS and Zone.js. Redux does it with the help of react hooks for the latest versions). We are going to simply do it using a setTimeout

function Component1Factory() {
const counter = 0;
return () => `<div>Component 1: ${counder}</div>`;
}

function Component2Factory() {
const counter = 0;
return () => `<div>Composnat 2: ${counter}</div>`;
}

const Component1 = Component1Factory();
const Component2 = Component2Factory();

function App() {
const appContainer = document.getElementById('app');
return () => {
appContainer.innerHTML = `
${Component1()}
${Component2()}
`
;
};
}

const renderFn = App();
setInterval(renderFn, 10);

In the Code above the renderFn will re-render the components every 10 milliseconds, and our goal is that when a component changes the counter, the other gets updated too.

We can just make counter a global variable, and when it’s updated, it’s updated everywhere. And that works, for our example. In a bigger application, we will for sure create the following same variables: data, payload, text, user, and session ( which I suggest you blacklist in your eslint ) and every piece of the code will have read-write access to them and it will be almost impossible to know how is the state changing and what’s changing it.

How about we create a class that holds the state and we make sure there is only one instance of this class ( Singleton ) so that when its state changes all the components will have the correct value (that’s what we call a Service in Angular).

Yes, the flux pattern is not the only pattern that allows for state-sharing and you can call it a day and use a Singleton pattern or some other pattern to manage your state. However, you’d be missing out on the assumptions you can make when using a flux architecture.

This is what a flux architecture looks like:

These are the following assumptions you can make when adopting it:

  • CQRS: You basically have a unified and unique way of writing state, which is different from the unified and unique way of reading state. So the assumption you can make is that if the state was changed, then there must be an action that was dispatched to change it.
  • Reducers let you make the following assumption: If my action was triggered, and my state doesn’t look like what I expect it to look like, my reducer isn’t working correctly, on the other hand, if my reducer works correctly and I test it thoroughly, I can guarantee that my state will change correctly when an action occurs.
  • Selectors: what I am reading is exactly what any other component is reading, and if my selector is thoroughly tested I will read exactly the same part of the state.
  • Knowing the history of actions that happened, you can know how your state evolved, and you can playback what happened to your state.

Now let’s dive in and create our own flux architecture, let’s start with what we would like to get:

const [createReducer, dispatch, select] = fluxStoreFactory(name);

The first function “createReducer” will allow us to register pure functions that will describe how the state changes when a certain action is triggered, it would look something like this:

createReducer("increment", (state, increment) => {
return {
...state,
count: state.count + increment,
};
});

When the action increment happens, the state must change like this, the count must be overridden by the old count + the increment.

Disptach is a simple function that allows us to dispatch actions with parameters ( or without parameters) but it should look something like this:

dispatch("increment", 1)

select is a function that takes a slice of the whole state and gives it back to us, it should be like this:

select((state) => state.count);

let’s implement our first function, createReducer

function reduxStoreFactory(initialState = {}) {
const state = initialState;
let actions = {};
return [
(action, reduce) => {
actions = {
...actions,
[action]: [...(actions[action] || []), reduce],
};
}
] as const;
}

This function is simple, whenever we create a new reducer we add it to an array of the corresponding action, it will look something like this:

{
"increment": [reducer1ForIncrement, reducer2ForIncrement...]
}

And whenever the increment happens, all the reducers will execute.

Now let’s move to the dispatch function

function reduxStoreFactory(initialState = {}) {
const state = initialState;
let actions = {};
const dispatch = (action, params) => {
Object.assign(
state,
(actions[action] || []).reduce(
(accState, reducer) => reducer(accState, params),
state
)
);
};
return [
(action, reduce) => {
actions = {
...actions,
[action]: [...(actions[action] || []), reduce],
};
},
dispatch,
] as const;
}

The dispatch function will take the current state, pick all the reducers of the action “actions[action]” and use each reducer to reduce the state to the new state, yeah, it’s that simple.

Next, we need to be able to select the state, that’s a one-liner,

(selector) => selector(state)

And if we put it all together:

function reduxStoreFactory(initialState = {}) {
const state = initialState;
let actions = {};
const dispatch = (action, params) => {
Object.assign(
state,
(actions[action] || []).reduce(
(accState, reducer) => reducer(accState, params),
state
)
);
};
return [
(action, reduce) => {
actions = {
...actions,
[action]: [...(actions[action] || []), reduce],
};
},
dispatch,
(selector) => selector(state),
];
}

const [createReducer, dispatch, select] = reduxStoreFactory();

createReducer('increment', (state, increment) => {
return {
...state,
count: (state.count || 0) + increment,
};
});

function Component1Factory() {
setInterval(() => dispatch('increment', 1), 1000);
return () => `<div>Componsant 1: ${select((state) => state.count)}</div>`;
}

function Component2Factory() {
return () => `<div>Composnat 2: ${select((state) => state.count)}</div>`;
}

const Component1 = Component1Factory();
const Component2 = Component2Factory();

function App() {
const appContainer = document.getElementById('app');
return () => {
appContainer.innerHTML = `
${Component1()}
${Component2()}
`
;
};
}

const render = App();

setInterval(render, 10);

And that’s what a flux architecture is all about. But to be completely honest, there is a small part missing, we’ve said that the reducer is a pure function, and one of the implications of that is that it can never have a side effect, like an api call, but in reality most of our state comes from the api, so how do we do that ?

Two ways, The first is to trigger a dispatch after calling the api, it would look something like this:

function countApi() {
return new Promise((res) => {
setTimeout(() => res(1), 100);
})
}

function Component1Factory() {
setInterval(() => {
countApi().then((increment) => dispatch("increment", increment))
}, 1000);
return () => `<div>Componsant 1: ${select((state) => state.count)}</div>`;
}

With this way of doing things you have to manage the cancelation of your request when needed, and what to do when it crashes.

The other way is using what we call effects, something that listens of actions, and when an action happens it executes, does what it has to do and at the end dispatch another action. the api would look something like this:

function reduxStoreFactory(initialState = {}) {
const state = initialState;
let actions = {},
effects = {};
const dispatch = (action, params) => {
Object.assign(
state,
(actions[action] || []).reduce(
(accState, reducer) => reducer(accState, params),
state
)
);
(effects[action] || []).forEach(({ effect, dispose }) => {
dispose();
effect(dispatch);
});
};
return [
(action, reduce) => {
actions = {
...actions,
[action]: [...(actions[action] || []), reduce],
};
},
dispatch,
(selector) => selector(state),
(action, effectFn) => {
const { effect, dispose } = effectFn();
effects = {
...effects,
[action]: [...(effects[action] || []), { effect, dispose }],
};
},
];
}

const counterApi = (signal) =>
new Promise((res) =>
setTimeout(() => {
if (!signal.aborted) res(1);
}, 100)
);
const [createReducer, dispatch, select, registerEffect] = reduxStoreFactory();

createReducer('increment success', (state, increment) => {
return {
...state,
count: (state.count || 0) + increment,
};
});

registerEffect('increment', () => {
let controller = new AbortController();

return {
effect: (dispatch) =>
counterApi(controller.signal).then((increment) =>
dispatch('increment success', increment)
),
dispose: () => {
controller.abort();
controller = new AbortController();
},
};
});

function Component1Factory() {
setInterval(() => dispatch('increment'), 1000);
return () => `<div>Componsant 1: ${select((state) => state.count)}</div>`;
}

function Component2Factory() {
return () => `<div>Composnat 2: ${select((state) => state.count)}</div>`;
}

const Component1 = Component1Factory();
const Component2 = Component2Factory();

function App() {
const appContainer = document.getElementById('app');
return () => {
appContainer.innerHTML = `
${Component1()}
${Component2()}
`
;
};
}

const render = App();

setInterval(render, 10);

the dispose is necessary as you have to cancel the first effect before starting a new one, otherwise, you might run into a race condition, for example, if the first request takes longer than the second one, you will get the second result and that’s not desirable.

Also, Note that the action dispatched by the effect must be different from the one the effect listens to, otherwise, you might get stuck in an infinite loop.

And that’s it, you’ve got yourself a flux architecture.
Thank you for reading

In Plain English 🚀

Thank you for being a part of the In Plain English community! Before you go:

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response