Put a Soul into a React-Redux Project (using Actors for Business Logic)
Tags: react, redux, javascript
Published: 2022-09-01 15:35:09

Imaging we have **ReactJS** for **UI** and **Redux** for **store data**. Users see a button, click on it, redux action happens, data stored in the global redux state and button change visual style. This looks like just a cause and an effect. It feels lifeless. Such applications are similar to primitive species.

Another thing - a button that after clicking waits for a second, simulating the thought process, and then changes the UI. And then waits one more second and turns itself off.
In this article I will try to explain how to add a brain into an application and making it smarter. You can call this as “adding a **Soul** to a React+Redux project” or "using **Actors** for **business logic**". I call it “**Ghost** in the **React**”. 


![React-Redux-Ghost](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ryvpfc6ou43cwr7eqln4.png)

User interact with React components. React components call actions. Reducer change state in the Redux. 

Now the Ghost comes into play. Ghost sees that the state has changed and does some work and calls actions to save the result in the Redux. React components see that state changed and show an updated UI to the user.

## Step 1. Install react with typescript

`npx create-react-app rg_example --template typescript`

You can find a lot of tutorials about. So go to next step.

## Step 2. Prepear page.

Let's clean up file App.tsx. 
```tsx
import React, {createContext, useContext, useMemo, useState} from 'react';
import './App.css';

type ContextInterface = {
	state: boolean;
	setState: (state: boolean) => void;
};

const Context = createContext<ContextInterface>({
	state: false,
	setState: (state: boolean) => undefined,
});

const App = () => {
	const [state, setState] = useState(false);
	const contextValue = useMemo(() => ({state, setState}), [state]);
	return <Context.Provider value={contextValue}>
		<Panel />
	</Context.Provider>;
};

const Panel = () => {
	const {state, setState} = useContext(Context);
	return (
		<div className='App'>
			<button onClick={() => {
				setState(!state);
			}} >{state ? 'disable' : 'enable'}</button>
			<span>{state ? 'enabled' : 'disabled'}</span>
		</div>
	);
};

export default App;
```

We added the button. When click - state change and button just change a text.

## Step 3. Adding a Ghost.

```tsx

const ButtonGhost = () => {
	const {state, setState} = useContext(Context);
	useEffect(() => {
		if (state) {
			const id = setTimeout(() => {
				setState(false);
			}, 1000);
			return () => {
				clearTimeout(id);
			};
		}
	}, [state, setState]);
	return null;
};

const App = () => {
	const [state, setState] = useState(false);
	const contextValue = useMemo(() => ({state, setState}), [state]);
	return <Context.Provider value={contextValue}>
		<Panel />
		<ButtonGhost />
	</Context.Provider>;
};

```

That's it. If you run this code you've already seen alive panel.

You click "enable" and it changes the state to "enabled". After 1 second Ghost changes state back to "disabled".

{% embed https://codesandbox.io/embed/suspicious-voice-xyidw5?fontsize=14&hidenavigation=1&theme=dark %}

## Step 3. Time to add redux.
Before we store state using the React Context and hook useState.
Let's move the panel state to the Redux.

At first install packages:
`npm i redux @reduxjs/toolkit use-store-path`

I don't like the `react-redux` package because I don't need all the functionality of this package. It's enough for me just subscribe to state change. So I use `use-store-path` instead.

```tsx
import React, {createContext, useContext, useEffect} from 'react';
import {createSlice, configureStore} from '@reduxjs/toolkit';
import type {PayloadAction} from '@reduxjs/toolkit';
import {getUseStorePath} from 'use-store-path';
import './App.css';

export const stateSlice = createSlice({
	name: 'flag',
	initialState: false,
	reducers: {
		set: (state, action: PayloadAction<boolean>) => action.payload,
	},
});

const store = configureStore({reducer: stateSlice.reducer});

const exStore = {
	...store,
	useStorePath: getUseStorePath(store),
};

const Context = createContext(exStore);

const App = () => <Context.Provider value={exStore}>
	<Panel />
	<ButtonGhost />
</Context.Provider>;

const Panel = () => {
	const {useStorePath, dispatch} = useContext(Context);
	const flag = useStorePath([]);
	return (
		<div className='App'>
			<button onClick={() => {
				dispatch(stateSlice.actions.set(!flag));
			}} >{flag ? 'disable' : 'enable'}</button>
			<span>{flag ? 'enabled' : 'disabled'}</span>
		</div>
	);
};

const ButtonGhost = () => {
	const {useStorePath, dispatch} = useContext(Context);
	const flag = useStorePath([]);
	useEffect(() => {
		if (flag) {
			const id = setTimeout(() => {
				dispatch(stateSlice.actions.set(false));
			}, 1000);
			return () => {
				clearTimeout(id);
			};
		}
	}, [flag, dispatch]);
	return null;
};

export default App;

```

> Please note that in this article we only store one flag to simplify the examples. In a real project, each reducer will have more values and actions.

## Step 4. Dynamic reducer.

Dynamic reducer is very useful in a large application. 
I use package `@simprl/dynamic-reducer` for it.

`npm i @simprl/dynamic-reducer`

Add useReducer hook to the application Context

```tsx
import {reducer as dynamicReducer} from '@simprl/dynamic-reducer';
import {Reducer} from 'redux';

const {reducer, addReducer} = dynamicReducer();

// before: const store = configureStore({reducer: stateSlice.reducer});
const store = configureStore({reducer});

const useReducer = (name: string, reducer: Reducer) => {
	useEffect(
		() => addReducer(name, reducer, store.dispatch),
		[name, reducer],
	);
};

const exStore = {
	...store,
	useStorePath: getUseStorePath(store),
	useReducer,
};
```

Now we can dynamically create reducer in the Ghost using `useReducer`. We just need every time define a space ("flag1") when dispatch action and subscribe to the Redux store.

```tsx
const ButtonGhost = () => {
	useReducer('flag1', stateSlice.reducer);
	const {useStorePath, dispatch} = useContext(Context);
	const flag = useStorePath(['flag1']);
	useEffect(() => {
		if (flag) {
			const id = setTimeout(() => {
				dispatch({...stateSlice.actions.set(false), space: 'flag1'});
			}, 1000);
			return () => {
				clearTimeout(id);
			};
		}
	}, [flag, dispatch]);
	return null;
};
```

## Step 5. Many spaces.

Now we can reuse same reducer in two spaces ("flag1" and "flag2")

```tsx
const App = () => <Context.Provider value={exStore}>
	<Panel space='flag1' />
	<Panel space='flag2' />
	<ButtonGhost space='flag1' />
	<ButtonGhost space='flag2' />
</Context.Provider>;

type WithSpace = {
	space: string;
};

const Panel = ({space}: WithSpace) => {
	const {useStorePath, dispatch} = useContext(Context);
	const flag = useStorePath([space]);
	return (
		<div className='App'>
			<button onClick={() => {
				dispatch({...stateSlice.actions.set(!flag), space});
			}} >{flag ? 'disable' : 'enable'}</button>
			<span>{flag ? 'enabled' : 'disabled'}</span>
		</div>
	);
};

const ButtonGhost = ({space}: WithSpace) => {
	useReducer(space, stateSlice.reducer);
	const {useStorePath, dispatch} = useContext(Context);
	const flag = useStorePath([space]);
	useEffect(() => {
		if (flag) {
			const id = setTimeout(() => {
				dispatch({...stateSlice.actions.set(false), space});
			}, 1000);
			return () => {
				clearTimeout(id);
			};
		}
	}, [flag, dispatch]);
	return null;
};

export default App;
```
Button Component and Ghost can work with different spaces. So we can reuse button, ghost and reducer with the same functionality but for different space in the Redux.

## Step 6. Click handler.

But that is not all. You might have noticed that we always recreate pointer to the click function. It leads to a rerender memoized component because we put a new property each time.

Usually it fix by `useCallback`

```tsx
const Panel = ({space}: WithSpace) => {
	const {useStorePath, dispatch} = useContext(Context);
	const flag = useStorePath([space]);

	const clickHandler = useCallback(() => {
		dispatch({...stateSlice.actions.set(!flag), space});
	}, [space, flag]);

	return (
		<div className='App'>
			<button onClick={clickHandler} >{flag ? 'disable' : 'enable'}</button>
			<span>{flag ? 'enabled' : 'disabled'}</span>
		</div>
	);
};
```
But you can see property 'flag' and 'space' in the deps. So we still have similar problem. On each click this 'flag' property changes and we again recreate function. Actually pointer to the handler should not depends from 'flag' and 'space' properties. But when handler call we need to get values of these properties from the last render.
I use hook useConstHandler from package `use-constant-handler`

`npm i use-constant-handler`

```tsx
import {useConstHandler} from 'use-constant-handler';

const clickHandler = useConstHandler(() => {
	dispatch({...stateSlice.actions.set(!flag), space});
});
```

useConstHandler every time returns a constant pointer to the function, but the context of this function changes every render - it means all properties in this function are always actual.

## Step 7. useAction hook.

Usually in my projects I create hook for call action.

```tsx
const useSpaceAction = (
	space: string,
	actionCreator: () => AnyAction
) => useConstHandler(() => {
	store.dispatch({space, ...actionCreator()});
});

const exStore = {
	...store,
	useStorePath: getUseStorePath(store),
	useReducer,
	useSpaceAction,
};
```

Now Panel component look like this

```tsx
const Panel = ({space}: WithSpace) => {
	const {useStorePath, useSpaceAction} = useContext(Context);
	const flag = useStorePath([space]);

	const clickHandler = useSpaceAction(space, () => stateSlice.actions.set(!flag));

	return (
		<div className='App'>
			<button onClick={clickHandler} >{flag ? 'disable' : 'enable'}</button>
			<span>{flag ? 'enabled' : 'disabled'}</span>
		</div>
	);
};

```

## Step 8. Separation into UI and business logic.

Good practice is separate UI and business logic. Let's do it.

```tsx
const App = () => <Context.Provider value={exStore}>
	<div className='App'>
		<AppUi />
	</div>
	<AppGhost/>
</Context.Provider>;
```
Now we have two laysers of the application:
- AppUi for UI
- AppGhost for business logic

Let's implement them and add more interactivity

```tsx
const AppGhost = () => {
	const {useStorePath} = useContext(Context);
	const flag1 = useStorePath(['flag1']);
	return <>
		<ButtonGhost space='flag1' />
		{flag1 && <ButtonGhost space='flag2' />}
	</>;
};

const AppUi = () => {
	const {useStorePath} = useContext(Context);
	const flag1 = useStorePath(['flag1']);
	return <>
		<Panel space='flag1' />
		{flag1 && <Panel space='flag2' />}
	</>;
};
```
Second panel and ghost render only if flag1 === true.

Note that this is also an example of the advantage of a dynamic reducer. Reducer for flag2 will add only if flag1 is true and will remove when flag1 === false

## Step 9. Don't use JSX for business logic.
Using JSX look strange for business logic. We can get confused if we use JSX in both cases. We need to somehow distinguish the business logic code from the UI code. I suggest to use the keywords 'ghost' and 'ghosts'. 

```tsx
import { createElement, Fragment } from 'react';

const ghost = createElement;
const ghosts = (...children) => createElement(Fragment, null, ...children);
```

To standardize this I use the same package `react-ghost` in all my projects.

`npm i react-ghost`

Lets rewrite AppGhost without jsx
```tsx
import {ghost, ghosts} from 'react-ghost';

const AppGhost = () => {
	const {useStorePath} = useContext(Context);
	const flag1 = useStorePath<boolean>(['flag1']);
	return ghosts(
		ghost(ButtonGhost, {space: 'flag1'}),
		flag1 && ghost(ButtonGhost, {space: 'flag2'}),
	);
};
```

## Result

Entire file:

```tsx
import React, {createContext, useContext, useEffect} from 'react';
import {createSlice, configureStore, AnyAction} from '@reduxjs/toolkit';
import type {PayloadAction} from '@reduxjs/toolkit';
import {reducer as dynamicReducer} from '@simprl/dynamic-reducer';
import {getUseStorePath} from 'use-store-path';
import './App.css';
import {Reducer} from 'redux';
import {useConstHandler} from 'use-constant-handler';
import {ghost, ghosts} from 'react-ghost';

export const stateSlice = createSlice({
	name: 'flag',
	initialState: false,
	reducers: {
		set: (state, action: PayloadAction<boolean>) => action.payload,
	},
});

const {reducer, addReducer} = dynamicReducer();
const store = configureStore({reducer});
const useReducer = (name: string, reducer: Reducer) => {
	useEffect(
		() => addReducer(name, reducer, store.dispatch),
		[name, reducer],
	);
};

const useSpaceAction = (space: string, actionCreator: () => AnyAction) => useConstHandler(() => {
	store.dispatch({space, ...actionCreator()});
});

const exStore = {
	...store,
	useStorePath: getUseStorePath(store),
	useReducer,
	useSpaceAction,
};

const Context = createContext(exStore);

const App = () => <Context.Provider value={exStore}>
	<div className='App'>
		<AppUi />
	</div>
	<AppGhost/>
</Context.Provider>;

type WithSpace = {
	space: string;
};

const Panel = ({space}: WithSpace) => {
	const {useStorePath, useSpaceAction} = useContext(Context);
	const flag = useStorePath([space]);

	const clickHandler = useSpaceAction(space, () => stateSlice.actions.set(!flag));

	return (
		<div>
			<button onClick={clickHandler} >{flag ? 'disable' : 'enable'}</button>
			<span>{flag ? 'enabled' : 'disabled'}</span>
		</div>
	);
};

const ButtonGhost = ({space}: WithSpace) => {
	useReducer(space, stateSlice.reducer);
	const {useStorePath, dispatch} = useContext(Context);
	const flag = useStorePath([space]);
	useEffect(() => {
		if (!flag) {
			const id = setTimeout(() => {
				dispatch({...stateSlice.actions.set(true), space});
			}, 1000);
			return () => {
				clearTimeout(id);
			};
		}
	}, [flag, dispatch]);
	return null;
};

const AppUi = () => {
	const {useStorePath} = useContext(Context);
	const flag1 = useStorePath(['flag1']);
	return <>
		<Panel space='flag1' />
		{flag1 && <Panel space='flag2' />}
	</>;
};

const AppGhost = () => {
	const {useStorePath} = useContext(Context);
	const flag1 = useStorePath<string>(['flag1']);
	return ghosts(
		ghost(ButtonGhost, {space: 'flag1'}),
		flag1 && ghost(ButtonGhost, {space: 'flag2'}),
	);
};

export default App;
```

You can play with this in codesandbox:

{% embed https://codesandbox.io/embed/distracted-fermat-eyvsy7?fontsize=14&hidenavigation=1&theme=dark %}

## Source code
Source code is available on [github](https://github.com/simprl/rg_example)
You can check each step in the [commit history](https://github.com/simprl/rg_example/commits/main)

## FAQ

### Why don't you just use redux middleware (your own or Thunk or Saga)?
Middleware doesn't have hooks.

### Why don't you just use hooks instead of ghost?
Hooks can’t have conditions. You cannot make a dynamic composition of hooks. So while it is not a problem - you can use hooks.