How to properly migrate to effector from redux, keeping actions pure and isolated?

235 Views Asked by At

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?

1

There are 1 best solutions below

0
Sergey Volkov On

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

import { createEffect } from "effector";

type SomeDataType = any;
type Input = void;
type Output = { someData: SomeDataType };

const fetchSomeDataFx = createEffect<Input, Output>(async () => {
  return await httpClient({
    url: '/api/some-endpoint',
    method: EMethodTypes.GET,
  })
})

P.S. if you need to debug this effect, you can use debug from patronum library

import { debug } from "patronum";

debug(fetchSomeDataFx);  

If you need to display information about fails http request you can use

import { createStore } from "effector";

export const $fetchSomeDataError = createStore<Error | null>(null)
  .reset(fetchSomeDataFx)
  .on(fetchSomeDataFx.failData, (_, error) => error);

And attach this effect to store

import { sample, createStore } from "effector";

export const $someData = createStore<SomeDataType | null>(null);

sample({
  source: fetchSomeDataFx.doneData,
  fn: (doneData) => doneData.someData,
  target: $someData
})

Hope it helps to you!