Skip to content

Commit

Permalink
Add more docs on migration
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexandrHoroshih committed Dec 12, 2023
1 parent 1aee5df commit 1e00656
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 13 deletions.
7 changes: 6 additions & 1 deletion apps/website/docs/redux-interop/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ outline: [2, 3]
# @withease/redux

Minimalistic package to allow simpler migration from Redux to Effector.
Also can handle any other usecase, where one needs to communicate with Redux Store from Effector's code.

This is a API reference article, for the Redux -> Effector migration guide [see the "Migrating from Redux to Effector" article](/redux-interop/usage).

Expand Down Expand Up @@ -63,6 +64,8 @@ const $user = reduxInterop.fromState((x) => x.user);

It is useful to mirror some part of the Redux state into Effector's world.

Notice, that `fromState` method supports Redux Store typings, which can be useful

#### `reduxInterop.dispatch`

This is a Effector's Event, which calls are redirected into Redux Store's `dispatch` method.
Expand Down Expand Up @@ -125,7 +128,7 @@ test('username updated after save button click', async () => {

const scope = fork({
values: [
// Providing mock version of the redux store, *if needed*
// Providing mock version of the redux store
[reduxInterop.$store, mockStore],
// Mocking anything else, if needed
[$nextName, 'updated'],
Expand All @@ -143,3 +146,5 @@ test('username updated after save button click', async () => {
```

☝️ This test will be especially useful in the future, when this part of logic will be ported to Effector.

Notice, that it is recommended to create a mock version of Redux Store for any test like this, since the Store is containg state, which could leak between the tests.
171 changes: 159 additions & 12 deletions apps/website/docs/redux-interop/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ In order for Redux and Effector to communicate effectively with each other, a sp

You should do it by using `createReduxInterop` method of the `@withease/redux` somewhere near the Redux Store configuration itself.

Redux Toolkit `configureStore` is used here as an example, `@withease/redux` supports any kind of Redux Store.

```ts
// src/redux-store
import { createReduxInterop } from '@withease/redux';
import { configureStore } from '@reduxjs/tookit';

export const myReduxStore = configureStore({
// ...
Expand Down Expand Up @@ -68,25 +72,168 @@ After that, you have everything ready to start a gradual migration.
Now you have existing code with Redux" that implements the features of your product.
There is no point in stopping development altogether to migrate between technologies, this process should be integrated into the product development.

So you should do a "feature by feature" kind of migration.
It's a good idea to pick one of the existing features in your code, rewrite it to the new technology, and show the resulting Pull Request to your colleagues. This way you can evaluate whether this technology helps you solve your problems and how well it suits your team.

This is a list of cases with examples of organizing a migration from Redux code to Effector code.

### Migrating existing feature

First thing you need to do in that case is to create an Effector model somewhere, where you want to put a new implementation.

#### Effector API for the Redux code

At first new model will only contain a "mirrored" stores and events, which are reading and sending updates to Redux Store:

```ts
// src/features/user-info/model.ts
export const $userName = reduxInterop.fromState(
(state) => state.userInfo.name ?? ''
);
export const updateName = reduxInterop.dispatch.prepend((name: string) =>
userInfoSlice.updateName(name)
);
```

☝️ It is recommended to use `.prepend` API of `reduxInterop.dispatch` event to create separate Effector events, connected to their Redux action counterparts.

This model then can be used anywhere in place of classic actions and selectors.

E.g. an UI component:

```tsx
import { useUnit } from 'effector-react';

function UserInfoForm() {
const { name, nameUpdated } = useUnit({
name: $userName,
nameUpdated: updateName,
});

return (
<Wrapper>
<Input
value={name}
onChange={(e) => {
nameUpdated(e.currentTarget.value);
}}
/>
</Wrapper>
);
}
```

You can find [API reference of UI-framework integrations in the Effector's documentation](https://effector.dev/en/api/).

#### Testing

Now that we have the Effector API for the old code, we can write some tests for it, so that the behavior of the Redux code will be captured and we won't break anything when porting the feature implementation to Effector.

Notice, that we also need to create mock version of the Redux Store, so this test is independent of any other.

```ts
import { configureStore } from '@reduxjs/tookit';

import { $userName, updateName } from 'root/features/user-info';
import { reduxInterop } from 'root/redux-store';
import { appStarted } from 'root/shared/app-lifecycle';

test('username is updated', async () => {
const mockStore = configureStore({
// ...
});

In this case there are two possible situations:
const scope = fork({
values: [
// Providing mock version of the redux store
[reduxInterop.$store, mockStore],
],
});

1. An old feature is gradually migrated to new technologies, and its implementation uses both old and new approaches at the same time.
2. The whole new feature is written on new technologies, integrating with the old code through an adapter.
await allSettled(appStarted, { scope });

Let's break down these situations one by one
expect(scope.getState($userName)).toBe('');

### Old feature
await allSettled(updateName, { scope, params: 'John' });

TBD
expect(scope.getState($userName)).toBe('John');
});
```

Such tests will allow us to notice any changes in logic early on.

You can find more details about Effector-way testing [in the "Writing tests" guide in the documentation](https://effector.dev/en/guides/testing/).

#### Gradual rewrite

We can now extend this model with new logic or carry over existing logic from Redux, while keeping public API of Effector units.

```ts
// src/features/user-info/model.ts
export const $userName = reduxInterop.fromState(
(state) => state.userInfo.name ?? ''
);
export const updateName = createEvent<string>();

sample({
clock: updateName,
filter: (name) => name.length <= 20,
target: [
reduxInterop.dispatch.prepend((name: string) =>
userInfoSlice.updateName(name)
),
],
});
```

☝️ Effector's model for the feature is extended with new logic (name can't be longer than 20 characters), but the public API of `$userName` store and `updateName` event is unchanged and state of the username is still lives inside Redux.

#### Moving the state

### New feature
Eventually you should end up with a situation where:

1. The state of the feature is still stored in Redux
2. But all related logic and side-effects are now managed by the Effector
3. and all external consumers (UI-components, other features, etc) interact with the feature through its Effector-model.

After that you can safely move the state into the model and get rid of Redux-reducer for it:

```ts
// src/features/user-info/model.ts
export const $userName = createStore('');
export const updateName = createEvent<string>();

sample({
clock: updateName,
filter: (name) => name.length <= 20,
target: $userName,
});
```

☝️ Feature is completely ported to Effector, `reduxInterop` is not used here anymore.

If there is still code that consumes this state via the Redux Store selector, and there is currently no way to move that consumer to use the Effector model, it is still possible to "sync" the state back into Redux as a read-only mirror of the Effector model state:

```ts
// src/features/user-info/model.ts

// ...main code

// sync state back to Redux
sample({
clock: $userName,
target: [
reduxInterop.dispatch.prepend((name: string) =>
userInfoSlice.syncNameFromEffector(name)
),
],
});
```

When writing a new feature on new technologies, it is enough to have a minimal "bridge" of interaction with the old code. In our case, this bridge will be the `reduxInterop` object.
☝️ But it's important to make sure that this is a read-only mirror that won't be changed in Redux in any other way - because then there would be two parallel versions of this state, which would probably lead to nasty bugs.

For the sake of this example, let's assume that we are adding a premium account feature to our product. Let's assume for now that we just need to check if the user has a premium subscription and that the existing account logic is already described using Redux.
## New feature

In this case, we need to create a new Effector model for this product feature, and prepare a "mirror" of the user state from Redux.
Adding a new feature on Effector to a Redux project is not much different from the initial step of migrating an existing feature:

TBD
1. Any new code is written in Effector
2. Any dependencies to Redux Store should work through `reduxInterop` API

0 comments on commit 1e00656

Please sign in to comment.