moving from Redux to Effector. And why is DX here

moving from Redux to Effector. And why is DX here

Frontend development is very rich in various tools. New frameworks and libraries are released almost every day and unfortunately not all of them are equally useful or can make your product better. In addition, they differ in the level of convenience for the developer. There is such a concept DX – Developer eXperience – by analogy with UX. This is how convenient and intuitive it is for the developer to use a certain service.

My name is Anya, I am a frontend specialist at SimbirSoft with more than three years of experience in development. I have already managed to work with many tools, participated in a project where a huge program was transferred to new libraries, including replacing Redux with Effector. In this article, I want to share my thoughts about these state managers from the point of view of DX.

Yes, they have been compared many times, but my focus will be on how to write Effector code for common cases in Redux. I emphasize, DX is not about rational arguments, but about comfort, feng shui, etc. (you know what I’m talking about, right?…).

Looking ahead, I want to say that I liked Effector. And above all, its simplicity — yes, yes, one of our favorite principles KISS). And maybe I still feel some national pride about Effector, because it was developed by guys from Russia.

What are the advantages?

The following illustration is my attempt to visualize the use of the state manager in the application. The user’s name is displayed on the page, in the code it is the name variable, the value of which is acquired on request.

We can see that Redux creates a shared store. For it, all reducers from different parts of the project are combined into a common rootReducer. And waiting for the execution of asynchronous requests, for which a separate redux-saga library is responsible, is also provided. Sagas from the entire project are combined into a common rootSaga and transferred to the store.

Probably, everyone has had this – you develop a new feature, do everything as written, but it does not work. You spend half the day looking for a mistake, cursing in a gentle, silent way, and then – ah, father, you just didn’t wake up the saga upstairs. It’s not just a lot of extra code, it’s a lot of extra time and work.

No one can argue that in terms of boilerplate Redux is very revealing. Until now, many “age-old” projects contain parts on React class components, where the store is written on classic Redux, even without a toolkit. Remember how many folders there are (reducers, actions, selectors, sagas…) and how many files are in each folder.

And how great it is to debate such projects: props are transferred via connect, that is, by clicking on the name of a prop or function in the IDE, you will not get to the saga or reducer. This is related to the problem of analyzing the project for “dead code”. Sagas are not imported anywhere, so it is impossible to understand what is used and what is not using IDE tools such as find file references.

So, what makes Effector good:

  1. For Effector, there is no need to write a general page, it is enough to describe the model in a specific part of the project. Repository files use standard import-export methods.

  1. In addition, the creators of Effector emphasize its declarative nature – that is, you do not need to write down the order of actions, but you need to tell Effector what you want, and it will do (fewer letters, removal of “boilerplate”).

  1. The third stated advantage is the reduction of “more” developers, – Effector is clearer and easier to use. I subscribe.

Next, let’s talk about how to use Effector. In the article, I tried to talk about it in as simple words as possible with examples, but I still recommend looking at official documentation.

How to use Effector at work

Units

We store the data in storewe create using the createStore(initial value) function, the name usually begins with $:

const $name = createStore('');

For synchronous actions with data, we use event:

const setName = createEvent();

For asynchronous processes – effectusing the createEffect(async_function) function, the name usually ends with Fx:

const getNameFx = createEffect(fetchName);

Effect allows you to describe the processing of possible consequences of asynchronous action using methods.fail and .done (return the result of the execution of the asynchronous function – unsuccessful and successful, respectively), there are also .doneData and .failData (Returns the data field from the response). Additionally, the pending execution state is available from the property .pending.

Store, event and effect are units – the main entities of Effector. There is another fourth – domainit allows you to do various things with all the data in the application at the same time (testing, for example).

Connections

When entities are created, it is necessary to specify how they are related to each other. Effector offers many different ways to do this. One of them is connections. Connections are different, my love sample. Main fields:

We specify event in clock and target, but if you specify store, its change will be perceived as an event, and for effect all 4 methods .fail, .done, .failData and .doneData are events.

sample({
	clock: setName,
	target: $name
}) //когда сработает setName, данные будут записаны в $name

You can specify an array of events in both fields:

sample({
	clock: [setName, getNameFx.doneData],
	target: $name
}) //когда сработает setName или getNameFx.doneData, данные будут записаны в $name

You can add processing of data received from clock before passing to target:

sample({
	clock: [setName, getNameFx.doneData],
	fn: name => trim(name), 
	target: $name
})//когда сработает setName или getNameFx.doneData, данные будут обработаны функцией trim и записаны в $name

Data arrays often come from the back, the elements of which do not contain the id or key fields, which are so necessary for react or, for example, ready-made ant-design components. The place to add the treatment is here:

sample({
	clock: getDataFx.doneData,
	fn: data => data.map((item) => {...item, key: uuid()})
	target: $data
})

You can run target under the condition:

sample({
	clock: [setName, getNameFx.doneData],
	filter: name => name !== ‘Василий’,
	target: $name
}) //когда сработает setName или getNameFx.doneData, данные будут проверены и, если имя не равно «Василий», записаны в $name

You can specify an additional data source for processing:

sample({
		clock: [setName, getNameFx.doneData],
		filter: name => name !== ‘Василий’,
		source: $lastName,
		fn: (lastName, name) => trim(name) + « » + trim(lastName),
		target: $name
	  })//когда сработает setName или getNameFx.doneData, данные будут проверены и, если имя не равно «Василий», обработаны с помощью функции fn, которая в качестве аргументов примет данные исходного события, а также store, указанного в поле source, результат обработки будет записан в $name

There is a recommendation – to write two files to describe one repository:

  • describe and export units in one,

  • and in the other you describe the “data flow”, this file will contain only imports of units and no exports.

By the way, I want to draw your attention to the fact that there are many ways to do the same things in Effector. For example, the already mentioned case:

sample({
	clock: getNameFx.doneData,
	target: $name
}) 

It can be described in another way:

const getNameFx = createEffect(fetchName);
const $name = createStore(' ').on(getNameFx.doneData, (v) => v);

Or like this:

const $name = createStore(' ');
const getNameFx = createEffect(fetchName);
getNameFx.doneData.watch((data) => $name.setState(data));

Typification

The sample must combine clock and target types (clock must return what target needs). We specify types when creating units:

const $name = createStore<string>('');
const setName = createEvent<string>();

More complicated with effect:

Accordingly, if clock does not return the type required by target, fn must return the type for target.

to go

Another thing I used a lot. Gate.

Often, a component that uses data obtained from a query uses the construct:

useEffect({
		dispatch(setNameAction())
	  }, [])

I wrote dispatch(action()) as Redux should.
With Effector, you can write in the same way:

useEffect({
		getNameFx()
	  }, [])

And you can use the useGate hook or the Gate component. We will create it in the model:

const NameGate = createGate();
forward({from: NameGate.state, to: getNameFx})

And in the component, we will call using the component . Will work as using an effect. If the NameGate component will have props, then the parent will be updated when these props change.

In addition to the use of the effect, you can remove all use of the state (transfer them to the model as a pair of store and event), and it turns out that all data work can be entrusted to the Effector, which is also good from the point of view of division of responsibilities.

Instead of output

On my project, Effector was chosen for the new version of the product. The main reasons for switching from Redux were:

  • reducing the code base

  • lower entry threshold for developers as the project team is large and updated.

In general, developer tools have two opposing problems:

1. If the technology is very declarative (it is necessary to write “few letters”, everything is under the hood), then the developer ceases to understand how everything is arranged inside, loses his roots, poor thing. This is just about Effector.

2. If, on the contrary, the technology is more low-level, you need to understand why, and prescribe a lot of details – more understanding, but more fatigue, more attention is needed. It’s hard for the developer. And this is Redux.

I don’t have ready-made answers to choose. Maybe it can help you choose our other article. Above I described what I value Effector for, it remains to sum up:

  1. You need to write much less code.

  2. It’s intuitive how to manipulate data—creating variables and changing their values, either synchronously or asynchronously.

  3. There is no need to create a common repository for the entire program and take into account its availability during further development; it’s easier to “debug” existing code and examine dependencies (what comes from where).

And what’s the point of DX? I will repeat once again that the most important thing is difficult to describe in words, but personally, when using Effector, I have “Developer eXperience is much better”:)

It’s good that in the world of technology there are many different options, and you can choose tools that train somewhere and relax somewhere.

Thank you for attention!

Read more author’s materials for frontend developers from my colleagues in SimbirSoft social networks – VKontakte and Telegram.

Related posts