@withease/redux
Minimalistic package to allow simpler migration from Redux to Effector. Also, can handle any other use case, where one needs to communicate with Redux Store from Effector's code.
INFO
This is an API reference article, for the Redux -> Effector migration guide see the "Migrating from Redux to Effector" article.
Installation
First, you need to install package:
pnpm install @withease/reduxyarn add @withease/reduxnpm install @withease/reduxAPI
createReduxIntegration
Effector <-> Redux interoperability works through special "interop" object, which provides Effector-compatible API to Redux Store.
const myReduxStore = configureStore({
// ...
});
const reduxInterop = createReduxIntegration({
reduxStore: myReduxStore,
setup: appStarted,
});Explicit setup event is required to initialize the interoperability. Usually it would be an appStarted event or any other "app's lifecycle" event.
You can read more about this practice in the "Explicit start of the app" article.
Async setup
You can also defer interop object initialization and Redux Store creation.
The createReduxIntegration overload without explicit reduxStore allows you to pass the Store via setup event later.
// src/shared/redux-interop
export const startReduxInterop = createEvent<ReduxStore>();
export const reduxInterop = createReduxIntegration({
setup: startReduxInterop,
});
// src/entrypoint.ts
import { startReduxInterop } from 'shared/redux-interop';
const myReduxStore = configureStore({
// ...
});
startReduxInterop(myReduxStore);
// or, if you use the Fork API
allSettled(startReduxInterop, {
scope: clientScope,
params: myReduxStore,
});In that case the type support for reduxInterop.$state will be slightly worse and reduxInterop.dispatch will be no-op (and will show warnings in console) until interop object is provided with Redux Store.
☝️ This is useful, if your project has cyclic dependencies.
Interoperability object
Redux Interoperability object provides few useful APIs.
reduxInterop.$state
This is an Effector's Store, which contains the state of the provided instance of Redux Store.
It is useful, as it allows to represent any part of Redux state as an Effector store.
import { combine } from 'effector';
const $user = combine(reduxInterop.$state, (x) => x.user);TIP
Notice, that reduxInterop.$state store will use Redux Store typings, if those are provided. So it is recommended to properly type your Redux Store.
reduxInterop.dispatch
This is an Effector's Effect, which calls Redux Store's dispatch method under the hood. Since it is a normal Effect - it supports all methods of Effect type.
TIP
It is recommended to create separate events for each specific action via .prepend method of Effect.
const updateUserName = reduxInterop.dispatch.prepend((name: string) =>
userSlice.changeName(name)
);
sample({
clock: saveButtonClicked,
source: $nextName,
target: updateUserName,
});It is also possible to convert a Redux Thunk to Effect by using Effector's attach operator.
import { createAsyncThunk } from '@reduxjs/toolkit';
import { attach } from 'effector';
const someThunk = createAsyncThunk(
'some/thunk',
async (p: number, { dispatch }) => {
await new Promise((resolve) => setTimeout(resolve, p));
return dispatch(someSlice.actions.doSomething());
}
);
/**
* This is a redux-thunk, converted into an effector Effect.
*
* This allows gradual migration from redux-thunks to effector Effects
*/
const someThunkFx = attach({
mapParams: (p: number) => someThunk(p),
effect: interop.dispatch,
});
const promise = someThunkFx(42);
// ☝️ `someThunk` will be dispatched under the hood
// `someThunkFx` will return an Promise, which will be resolved once someThunk is resolvedreduxInterop.$reduxStore
This is an Effector's Store, which contains provided instance of Redux Store.
It is useful, since it makes possible to use Effector's Fork API to write tests for the logic, contained in the Redux Store!
So even if the logic is mixed between the two like this:
// app code
const myReduxStore = configureStore({
// ...
});
const reduxInterop = createReduxIntegration({
reduxStore: myReduxStore,
setup: appStarted,
});
// user model
const $user = combine(reduxInterop.$state, (x) => x.user);
const updateUserName = reduxInterop.dispatch.prepend((name: string) =>
userSlice.changeName(name)
);
sample({
clock: saveButtonClicked,
source: $nextName,
target: updateUserName,
});It is still possible to write a proper test like this:
test('username updated after save button click', async () => {
const mockStore = configureStore({
// ...
});
const scope = fork({
values: [
// Providing mock version of the redux store
[reduxInterop.$reduxStore, mockStore],
// Mocking anything else, if needed
[$nextName, 'updated'],
],
});
await allSettled(appStarted, { scope });
expect(scope.getState($userName)).toBe('initial');
await allSettled(saveButtonClicked, { scope });
expect(scope.getState($userName)).toBe('updated');
});☝️ This test will be especially useful in the future, when this part of logic will be ported to Effector.
TIP
Notice, that it is recommended to create a mock version of Redux Store for any tests like this, since the Store contains state, which could leak between the tests.