React redux and saga pattern.

Spread the love

This is a simple pattern that will provide react hook and react higher component to get the state from redux and register the actions.

First create a ts file with an initial state, reducers, and saga effects:

const namespace = "faltutech";

export interface istate {
	someVar: boolean;
}

export const initialState: istate = {
	someVar: false
};

xport const faltutechReducer: ReducerType = {
	namespace: namespace,
	reducers: {
		reload: (state, action) => {
			return { ...state, ...action.payload };
		}
	}
};

export const faltutechEffects: EffectType = {
	namespace: namespace,
	effects: {
		// Below effect is an example
		*toggleVarr({ put, call }, services, { payload }) {
			yield put(reload({...payload }));
		}
	}
};

// Services to be provided to saga (Effects)
export const faltutechServices = [];

////////////  Reducers ////////////////////////////

export const reload = (state: any) => {
	return { type: "faltutech/reload", payload: state };
};

/////////// Effects ///////////////////////////////
export const toggleVarr = (state: any) => {
	return { type: "faltutech/toggleVarr", payload: state };
};

In the above code, the namespace must be the first part of the type in effects and reducers and second should be the name of effect or reducer itself.

Now create the store.ts which will handle the rest of our abstraction:

export interface StateTypes {
	faltutech: istate;
}

// Add states here under unique namespace (key)
export const combinedState: StateTypes = {
	faltutech: initialState
};

const reducers = [faltutechReducer];
const effects = [faltutechEffects];
const services = { ...faltutechServices };

//******************************* All the configurations for new state are above. Don't change anything below ******************************//
// Reducer type
export interface ReducerType {
	namespace: string;
	reducers: any;
}

export interface EffectType {
	namespace: string;
	effects: any;
}

const generateNamespacedReducers = () => {
	// 'genericReducer' Calls the required reducer based on the function
	const genericReducer = (state, action, reducer: ReducerType) => {
		// find namespace and function name
		const values = action.type.split("/");
		const funcName = values[1];
		// if namespace is not found in action or function's name (funcName) is undefined or function is not present in reducer.reducers then return the state
		if (reducer.namespace !== values[0] || !funcName || !reducer.reducers[funcName]) {
			return { ...state };
		}
		// Call the reducer for the action
		return reducer.reducers[funcName](state, action);
	};

	// Create the collection of namespaced reducers.
	const namespacedReducers = {};
	reducers.map(r => {
		Object.assign(namespacedReducers, {
			[r.namespace]: (state = { ...combinedState[r.namespace] }, action) => genericReducer(state, action, r)
		});
	});
	return combineReducers(namespacedReducers);
};

// Fork sagas to run them in parallel. (If desired also search for spawn on saga docs)
const forkSagas = () => {
	return function*(sagaEffects, serviceObj) {
		yield all(
			[
				...effects.map(e => {
					return Object.keys(e.effects).map(v => fork(sagaWatcher(e.namespace, e.effects[v].name, e.effects[v]), sagaEffects, serviceObj));
				})
			].reduce((acc, val) => [...acc, ...val], [])
		);
	};
};
// Watcher watches for the particular action
const sagaWatcher = (namespace, funcName, func: any) => {
	return function*(sagaEffects, serviceObj) {
		yield takeLatest(`${namespace}/${funcName}`, func, sagaEffects, serviceObj);
	};
};

// Reducers combined
const reducersCombined = generateNamespacedReducers();
// Sagas forked
const sagasForked = forkSagas();

const saga = createSagaMiddleware();
export const store = createStore(reducersCombined, applyMiddleware(saga));
// Run saga middleware
saga.run(sagasForked, { put, call }, services);
// A connect function
export const connect = (mapStateToProps: (state: StateTypes) => {}, mapDispatchToProps: {}, component: React.FunctionComponent) => {
	const mapStateToPropsFunc = state => {
		return mapStateToProps(state);
	};
	const mapDispatchToPropsFunc = dispatch => {
		const allDispatchFunc = {};
		Object.keys(mapDispatchToProps).map(key => {
			const func = {
				[key]: state => dispatch(mapDispatchToProps[key](state))
			};
			Object.assign(allDispatchFunc, func);
		});
		return allDispatchFunc;
	};

	return reduxConnect(mapStateToPropsFunc, mapDispatchToPropsFunc)(component);
};

// A connect equivalent hook
export const useConnect = <T>(mapStateToProps: (state: StateTypes) => {}, mapDispatchToProps: {}): T => {
	const dispatch = useDispatch();
	const dispatcher = () => {
		const allDispatchFunc = {};
		Object.keys(mapDispatchToProps).map(key => {
			const func = {
				[key]: state => dispatch(mapDispatchToProps[key](state))
			};
			Object.assign(allDispatchFunc, func);
		});
		return allDispatchFunc;
	};
	const dispatcherObj = React.useMemo(() => dispatcher(), [dispatcher]);

	return [useSelector(mapStateToProps), dispatcherObj] as any;
};

Above code abstracts, watch functions, combining reducers, combining state, providing a hook for the functional components to get the state and register the actions.

All you need is to add the initial state, reducers, effects, services at the top of the above code.

Usage of the connect function:

export const sampleFunc = connect((({sampleVar}): StateTypes => ({faltutech: {sampleVar}})), {reload}, (props) => {
props.reload({sampleVar: true});
return <>{props.sampleVar}</>
});

In the above code we can access the reload function and sampleVar through props.

Use of the useConnect function:

export const sampleFunc = (props) => {
const [{sampleVar}, caller] = useConnect<[Type1, Type2]>((({faltutech: {sampleVar}}): StateTypes => ({sampleVar})), {reload})
caller.reload({sampleVar: true});
return <>{sampleVar}</>
}

If have any queries comment below.