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?
You should'n create effects inside a functions, because it is not performable and may cause memory lacks
Using your code snippet you can create effect
P.S. if you need to debug this effect, you can use
debugfrompatronumlibraryIf you need to display information about fails http request you can use
And attach this effect to store
Hope it helps to you!