Thanks to Redux, the journey to the world of amazing functional programming began for me. And this is the first of the functionalities that I tried in production. Gone are the days when I used DOM to store state and manipulated it uncertainly with jQuery.
Redux is a tool for managing the state of an application (state), which allows you to completely separate it from the view. The view becomes a state derivative that provides the user with an interface for changing it. User actions (actions) do not change the state directly. Instead, they fall into a reducer. This is such a pure function that, based on the previous state (state) and action (action), generates the next state (state). This approach to updating data was largely inspired by the Elm programming language architecture and the Flux unidirectional data flow concept. This is perhaps the most popular JavaScript library for an immutable state change from those that exist today. Redux authors focused on solving one single problem - managing the state of the application (state), and did it well. Redux was modular enough to work with different libraries to display the view.
React uses a similar focused approach to view (view), has an effective virtual DOM that can be connected to the browser DOM, native mobile applications, VR and other platforms.
To create reliable, functional, and easily debugged web applications, you can use React and Redux. True, you need support libraries like react-redux and a bunch of boilerplate code. And you can try Hyperapp .
Hyperapp is a single library that provides state management of the application (state) and immunity, as in Redux / Elm, in combination with view display (view) and Virtual DOM, as in React. Hyperapp uses functional programming approaches to manage its state, but it is more flexible when it comes to resolving side effects, asynchronous actions, and DOM manipulations. Hyperapp provides a powerful abstraction for creating web applications, but at the same time gives you full access to native APIs in order not to restrict you.
A simple counter application on React + Redux versus Hyperapp equivalent:
Let's go through each of the numbered code blocks and see what's what.
In Redux, a state can be of any type, although it is strongly recommended to choose a data type that is easy to serialize. The overwhelming majority of developers use an empty object as a state for a reducer (reducer).
Not reflected in the code, but a call to Redux.createStore
takes as an optional argument the initial state. In Hyperapp, a state is always an object. Instead of two different ways of state initialization, there is one here.
In Redux, action creators are functions that return actions, like JavaScript objects. Typically, action creators are connected to a Redux store (store) using bindActionCreators
, either automatically or manually, using the mapDispatchToProps
argument for ReactRedux.connect
. Actions are usually defined as multiple export from a single file, which is then drawn into a single namespace using import * as actions from "./actions"
when using ES6 modules.
In Hyperapp , action action generators, reducer (reducer) and bindActionCreators
not needed. Actions (actions) are pure functions that immutably change the state (state) and have all the data necessary for this.
In Redux, a state change occurs in a reducer (reducer), which is a pure function, takes a state (state) and an action (action), returning the next state (state). An action can update the state in any reducer.
The state change function is as follows:
(state, action) => nextState
Hyperapp uses the state change function of this form:
(action) => (state [, actions]) => nextState
Not reflected in the code, but Hyperapp performs state merge. Therefore, instead of Object.assign
or {... state, key: "value"}
you simply need to return: {key: "value"}
.
In Redux, the view must be manually connected to the state and the action creators. For this, you have to use the ReactRedux.connect
higher order function (HOC), which wraps your component to connect it to the Redux store (store). For this to work, you must also wrap your application in <ReactRedux.Provider>
, which makes your store available for any components that want to connect to it.
In Hyperapp, your state (state) and actions (actions) are automatically connected to your view (view), and only the top-level components have access to them.
Redux and Hyperapp maintain a state with nested namespaces, however they do this using slightly different approaches. In Hyperapp, this comes out of the box — in Redux, you need to manually use combineReducers
.
Redux code:
const potatoReducer = (potatoState = initialPotatoes, action) => { switch (action.type) { case FRY: // ... } } const tomatoReducer = (tomatoState = initialTomatoes, action) => { switch (action.type) { case GRILL: // ... } } const rootReducer = combineReducers({ potato: potatoReducer, tomato: tomatoReducer }) // This would produce the following state object { potato: { // ...potatoes // and other state managed by the potatoReducer... }, tomato: { // ...tomatoes // and other state managed by the tomatoReducer... // maybe some nice sauce? } }
Equivalent code using Hyperapp :
const rootState = { potato: { // ...just potato things }, tomato: { // ...just tomato things // maybe some nice sauce? } } const rootActions = { potato: { // these actions receive only // the potato state slice and actions }, tomato: { // these actions receive only // the tomato state slice and actions } }
An example of the organization of asynchronous actions (acions)
const actions = { upLater: value => (state, actions) => { setTimeout(actions.up, 1000, value) }, // Called one second after upLater up: value => state => ({ count: state.count + value }) }
import { withEffects, http } from "hyperapp-effects" const state = { // ... } const actions = { foo: () => http("/data", "dataFetched"), dataFetched: data => { // data will have the JSON-decoded response from /data } } withEffects(app)(state, actions).foo()
You can add your own effects.
To extend the capabilities of action creators, Redux suggests the use of applyMiddleware
at the store creation level.
Hyperapp involves manual composition of actions (actions) and middleware.
// Manual composition hoa3(hoa2(hoa1(app)))(state, actions, view, document.body) // Or with a standard-issue compose function compose(hoa3, hoa2, hoa1)(app)(state, actions, view, document.body) // Compose plays nicely with using different HOAs per environment const hoas = NODE_ENV === "production" ? productionHoas : devHoas compose(...hoas)(app)(state, actions, view, document.body)
A simple example of middleware is hyperapp-logger , which displays information on the console when you call any of your actions:
logger(options)(app)(state, actions, view, document.body)
Hyperapp takes simplicity as seriously as Redux. Make complex simple, and more less possible. The Hyperapp source code is ~ 300 lines of code that I can read when I have questions, or when debugging, when I have problems. Library size is only 1.4 kb.
I run away from my functional homeland of Redux, not because I don’t like it, but because of all the pain and suffering that its neighbors cause for me. If you like Redux, as I do, and are looking for a better balance between a simple functional world of data conversion and a complex external imperative world, then I recommend you give Hyperapp a chance.
Source: https://habr.com/ru/post/349810/