From Flux to Recoil; from functions that imitate classes to simple functions
React was released in July 2013 as a JavaScript library for creating user interfaces. We kept an eye on this technology for two years, and when version 0.13 came out, we began using it for new projects. For 5 years React has been one of our pillars for frontend development for both web and mobile. Five years is a long time, but the evolution of the library and its community has kept the technology current with modern trends.
This article describes how we evolved with React. Since React is not a framework, but only a library for displaying interfaces, developers should be concerned about how the interfaces interact with each other and the server. This article discusses the types of interaction structures we have used.
Programming Before React
Programmers are constantly learning, often from our own mistakes. In some instances we did not make the right decisions, but it is important to understand the factors at play when decisions were made. First let’s talk about what we used before React.
Most of our projects were developed using a Php Yii framework that uses a classic MVС pattern. For the frontend we used mostly jQuery. Projects with a complex interface, or when the interface had to work without rebooting the page (such as a radio station website) were developed using the Backbone.js library.
We will describe the main points of Backbone.js, but you can find additional information on their official website at https://backbonejs.org. Backbone is a library that consists of:
- Model – allows you to store an object, sync with the server (get/create/save data), notify subscribers about changes.
- Collection – contains a list of models, syncs with the server (gets data from a certain filter), notifies subscribers about changes.
- View – allows you to subscribe to the model/collection, make changes to DOM using the render method, track changes from DOM (for example, listening to input events and passing on to a model, via event bindings)
- Router – routing on the client’s side
Sync – By default, Backbone uses REST API to interact with the server. It is only necessary to specify the right URI for collections and models. But if you need something else, like use of a websocket, you can use this particular library component.
Also at the time, we used a standard underscore template engine, RequireJS, for lazy file and bundle downloads, and Grunt for building the entire app.
Backbone is also an MVС Framework, where the controller performs the view.
As pros, we identified fast data processing and coding styles. The downsides were constant binding of DOM elements with models, and a lot of issues with the render view function if we did not re-render the whole html from the template.
Our early experience with React, flux
When we got tired of Backbone and there was a new project where it was reasonable to use React, we took the opportunity to make the switch. From the React documentation at the time, we highlighted the following basic features:
- At the time, there were no classes in js, so we used the function React.createClass({, where we could write not only our functions, but also the functions that were called according to the component lifecycle.
- The function componentDidMount is called when the component integrates into the DOM. Here, we are usually able to subscribe to various events (such as from the store) or call a function that will send and return data to and from the server for that component.
- The function render is a mandatory feature that returns the “html” that is being built to its right place in the DOMThe function componentWillUnmount is called when the component is removed from the DOM. Here is where we unsubscribe from different events.
The function setState is used to assign values to a state, which causes re-rendering of the component.
Here’s what we liked about React, compared to Backbone:
- A more comprehensive integral approach, where components use other components. In JSX it looks like a regular HTML with attributes
- Event bindings are located near the element, for example <button onClick={this.handleButton}>кнопка</button>
- There is a state where we can store temporary data needed only for a specific component and the render is called by default
- And of course, React determines what has changed and makes changes to the DOM by itself
But React is just the View; therefore, we had to think about the architecture of the application − how to combine components, where to store the data required by multiple components, how to interact with the server. At that time there were two schools of thought. The first preferred to use a single store paradigm – Redux. The second, led by Facebook, offered the Flux architecture https://facebook.github.io/flux, which we chose because we thought that if React was maintained by Facebook, their architecture would be more appropriate. The following chart explains how it works:
Where:
- View – is a React component
- Store – is an EventEmitter that stores a certain value and sends a message to all subscribers if the values have changed
- Dispatcher – is a function that determines the Stores for data transferring
- Action – sends an action to Dispatcher
The short algorithm we had looked like this:
- The user pressed the Like button in the React component
- The function in the React component determined which element the button referred to and called for Action like (id)
- Action like sent data to the server and called the function Dispatcher with type=”like” and id
- Dispatcher passed and sent changes to those Stores, that process type=”like”
- After changing data, Stores sent an event to subscribers with changed data
- React components subscribed on Stores, after receiving events with changes, fetched new data and changed their state
- React re-rendered all components and made changes to the DOM, where the user could notice changes anywhere the Like feature was displayed.
Today React Router still helps us organize the structure of pages, even though its API has changed.
At first, we had a hard time using React and Flux. There were a lot of mistakes made, and we didn’t complete the project on time.
Moving on to Redux
We built one project on Flux, and the other used a similar architecture, but without the Store because the project did not need the same interaction between components. As a last resort, we were able to organize the exchange of data through a common parent.
Flux’s main problem was that the Stores sent messages to several components, which in turn changed the state, and for each change React built a virtual DOM and made comparisons of the changes. As a result, performance was reduced. At first, it never occurred to us that React worked with one virtual DOM. We thought everything was similar to Backbone – one render could only make changes to its part of the DOM. Once we realized our mistake, we started using a single Store – the architecture of Redux. You can get acquainted with it on their official website https://redux.js.org
Major changes made to our projects
- Actions dispatch action not to Dispatcher, but to Reducer
- Container layer, which connected the component with state redux and actions, by using the compose function
- Selector functions that were used in containers to filter and convert data from state redux to data needed by the components
Here is a brief algorithm of the interactions of the system components:
- The user presses the Like button in the React component
- The function in the React component determines which element the button refers to and calls the function in Container like (id),
- The Container-like function calls the corresponding Action function through dispatch (like (id))
- Action like sends data to the server and calls through the dispatch function redux with type=”like” and id
- Redux passes through all the reducers and assembles a new state tree, and those reducers that expect type type=”like” return their altered state
- Since the state has been altered, the virtual DOM rendering is launched
- Components receive new data via containers and thus React makes changes to the DOM
This architecture has satisfied us for a long time. We only played with libraries, which allowed for optimizing the work of Action, and eliminating the need to write duplicate code.
We tried using a similar architecture when developing React Native apps, but one time we got burned. We had a business task to arrange a coworking space reservation. For convenience, the whole process was broken down into several steps or screens:
- Space selection
- Date choice
- Time choice
- Payment and return to the menu
With each step, data was updated in the reducer, and at the last step the payment was made, the reservation was recorded into the database, the data in the reducer was cleared and there was a return to the first screen. The application crashed because we cleared data before react-navigation removed the screens from the stack. For some reason, we thought that only one screen would work at a single point in time. We fixed this problem simply by processing the absence of data in the right form. Moving forward, we were less inclined to use Redux for mobile apps, and instead operated within the parameters of the screen provided by React-navigation.
Trying contexts
For a while, we used Redux in mobile applications because somewhere we needed to store shared data that could be used by multiple components/containers, such as authorized user information. At some point, the contexts were brought into React and we started using them instead of Redux.
The function of contexts is clear:
Somewhere you need to create Context with default values
const MyContext = React.createContext(defaultValue);
Somewhere in the above component tree we create a Provider
<MyContext.Provider value={/* some value */}>
Where you need to get data from the context, you need to use Consumer
<MyContext.Consumer>
{value => /* render something based on the context value */}
</MyContext.Consumer>
As a result, if we do not use one global context, but use several, breaking the logic, it leads to the following ugly code:
const Store = ({children}) => (
<Spinner>
<Device>
<PushNotification>
<CurrentUser>
<Settings>
<Currencies>
{children}
</Currencies>
</Settings>
</CurrentUser>
</PushNotification>
</Device>
</Spinner>
);
And a similar ladder where these contexts are used, although when we started using hook-and, the ladder decreased. The main change brought by contexts to our architecture is the use of thick containers. Business logic was already executed there, and the repeating code was taken out in separate functions and files. Because of the inconveniences, we continued to use contexts in projects where we needed no more than 2 global states. When we required more states, we continued to use Redux.
Recoil. This is already interesting
Many popular libraries have started using hook-and or similar syntax. It became very convenient to use Apollo Client. And working with State or Redux has become more convenient and clear. Redux is still more convenient for getting data, but not very convenient for updating data. Here is an example from the official documentation:
import React from ‘react’
import { useDispatch, useSelector } from ‘react-redux’
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
const counter = useSelector(state => state.counter)
return (
<div>
<span>{counter}</span>
<button onClick={() => dispatch({ type: ‘increment-counter’ })}>
Increment counter
</button>
</div>
)
}
import React from ‘react’
import ReactDOM from ‘react-dom’
import { createStore } from ‘redux’
import { Provider } from ‘react-redux’
function counter(state = 0, action) {
switch (action.type) {
case ‘increment-counter’:
return state + 1
case ‘decrement-counter’:
return state – 1
default:
return state
}
}
let store = createStore(counter)
const rootElement = document.getElementById(‘root’)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
)
As you can see, the inconvenience of using action.type remains, because in more complex situations it is difficult to keep track of what should be in the payload for a particular type.
Recently, we have been considering the next development from Facebook – Recoil https://recoiljs.org A small example from the documentation demonstrates its ease of use:
import {atom, useRecoilState} from ‘recoil’;
const counter = atom({
key: ‘myCounter’,
default: 0,
});
function Counter() {
const [count, setCount] = useRecoilState(counter);
const incrementByOne = () => setCount(count + 1);
return (
<div>
Count: {count}
<br />
<button onClick={incrementByOne}>Increment</button>
</div>
);
}
It looks like the usual application of useState, but if you consider that the library supports selector, getter, setter, and synchronous execution, and the same atom can be used in different components, then Recoil is a great Redux replacement.
To learn more about Recoil, follow the link, or stay tuned for our upcoming article about Recoil.
Conclusions
React continues to be the leader over other similar libraries for several reasons:
- Minimalism. React is View; you choose the architecture of the application yourself, and it can be changed over time
- On-trend. React is frequently updated
- Mobile development. As a technology. React is well suited for mobile development, as long as there is no simpler alternative
- Component approach. Many ready-made components allow for faster application development
We periodically look at new alternatives, but so far there are not many options that outperform React.