Migration guide to effector (effector.dev) from redux with illustrative examples (React)
I ve decided to try effector
and I m trying to figure out the best way to replace redux in my project with it.
Whenever I use redux in a React project, I usually have the following structure for a feature:
src
└── features
└── some_feature
├── components
│ └── MyComponent
│ └── index.ts
└── redux
├── actions.ts
├── types.ts
└── reducer.ts
With the said, here is how my files would look like:
// src/features/some_feature/components/MyComponent/index.ts
import * as React from react ;
import { useDispatch } from react-redux ;
import { someFunction } from ../../redux/actions ;
const MyComponent: React:FC = () => {
const dispatch = useDispatch();
React.useEffect(() => {
dispatch(someFunction());
}, [])
return <div>My component!</div>
}
My actions:
// src/features/some_feature/redux/actions.ts
import { httpClient } from src/services/httpClient ;
import { SOME_ACTION } from ../types ;
type TSetSomeData = {
payload: {
someData: any;
}
}
const setSomeData = ({ payload: { someData } }): ThunkAction =>
(dispatch): void => {
dispatch({
type: SOME_ACTION,
payload: someData
})
};
export const someFunction = (): ThunkAction =>
async (dispatch): Promise<void> => {
try {
const { someData } = (await httpClient({
url: /api/some-endpoint ,
method: EMethodTypes.GET,
})) as {
someData: any;
};
dispatch(setSomeData({ payload: { someData } }));
} catch (err) {
console.log( Error in someFunction , err);
}
};
My reducer:
// src/features/some_feature/redux/reducer.ts
import { AnyAction } from redux ;
import { SOME_ACTION } from ./types ;
export type ISomeFeatureState = {
someData: any;
};
const initialState = {
someData: null,
};
const someFeatureReducer = (state: ISomeFeatureState = initialState, action: AnyAction): ISomeFeatureState => {
const { type, payload } = action;
if (type === SOME_ACTION) {
return {
...state,
someData: payload,
};
} else {
return {
...state,
};
}
};
export default someFeatureReducer;
And types.ts
would have export const SOME_ACTION = @redux/features/some_feature/some-action
.
Anyway, here is how my folder structure looks like now:
src
└── features
└── some_feature
├── components
│ └── MyComponent
│ └── index.ts
└── effector
├── actions.ts
├── events.ts
└── store.ts
And here are the files:
// src/features/some_feature/effector/store.ts
import { createStore } from effector ;
export const $someData = createStore(null, {
updateFilter: (someData) => !!someData,
});
// src/features/some_feature/effector/events.ts
import { $someData } from ./store ;
export const setSomeDataEvent = createEvent();
$someData.on(setSomeDataEvent, (state, payload) => payload);
// src/features/some_feature/effector/actions.ts
import { setSomeDataEvent } from ./events ;
type TSetSomeData = {
payload: {
someData: any;
};
};
export const setSomeData = ({ payload: { someData } }: TSetSomeData) => {
setSomeDataEvent(someData);
};
So, it s already cleaner and less code. The reason I ve chosen such a structure and approach, is because it is very similar to my existing structure.
Anyway, effector offers different ways of mutating stores, one of which is on doneData
:
// from the docs
import { createEvent, createStore, createEffect, sample } from effector
const nextPost = createEvent()
const getCommentsFx = createEffect(async postId => {
const url = `posts/${postId}/comments`
const base = https://jsonplaceholder.typicode.com
const req = await fetch(`${base}/${url}`)
return req.json()
})
const $postComments = createStore([])
.on(getCommentsFx.doneData, (_, comments) => comments)
const $currentPost = createStore(1)
.on(getCommentsFx.done, (_, {params: postId}) => postId)
sample({
source: $currentPost,
clock: nextPost,
fn: postId => postId + 1,
target: getCommentsFx,
})
nextPost()
Here, once getCommentsFx
finishes executing, the value for the store $postComments
is set with whatever getCommentsFx.doneData
resolves to.
What I m having trouble with, is utilizing the same approach, but making it "friendly" with my current project.
The only way that I can think of, is rewriting someFunction
like this:
// src/features/some_feature/effector/actions.ts
import { createEffect } from effector ;
import { httpClient } from src/services/httpClient ;
import { $someData } from ../store ;
import { setSomeDataEvent } from ../events ;
type TSetSomeData = {
payload: {
someData: any;
}
}
export const setSomeData = ({ payload: { someData } }: TSetSomeData) => {
setSomeDataEvent(someData);
};
export const someFunction = (): ThunkAction =>
async (dispatch): Promise<void> => {
try {
const { someData } = await createEffect<{someData: any}>(async () =>
httpClient({
url: /api/some-endpoint ,
method: EMethodTypes.GET,
})
);
setSomeDataEvent(someData);
} catch (err) {
console.log( Error in someFunction , err);
}
};
But I see no point in using createEffect
at all, because I can just do this:
export const someFunction = (): ThunkAction =>
async (dispatch): Promise<void> => {
try {
const { someData } = (await httpClient({
url: /api/some-endpoint ,
method: EMethodTypes.GET,
})) as {
someData: any
}
setSomeDataEvent(someData);
} catch (err) {
console.log( Error in someFunction , err);
}
};
Any suggestions about going about this? Is it fine to use effector
without createEffect
(or most other provided methods via its API, for that matter)? Essentially, I m just creating stores and binding events to them, I feel like I m not using effector
in the way it was intended, but I can t think of a better way to rewrite it.
What can I do here? Should I just got back to Redux?