📜 ⬆️ ⬇️

Idiomatic Redux: Redo's Tao, Part 1 - Realization and Design

Thoughts on what requirements Redux puts forward, how the use of Redux is intended and what is possible with Redux.


Introduction


I spent a lot of time discussing online patterns of using Redux, whether it was helping those who study Redux in Reactiflux channels , discussions about possible changes to the Redux library API on Github, or discussing various aspects of Redux in the comments to threads on Reddit 'e or HN (HackerNews) . Over time, I developed my own opinion about what constitutes good, idiomatic Redux code, and I would like to share some of these thoughts. Despite my status as a maintainer of Redux, these are just opinions , but I prefer to think that they are good enough approaches.


Redux, in its essence, is an incredibly simple pattern. It saves the value, performs one function to update the value when needed, and notifies any subscribers that something has changed.


Despite this simplicity, or perhaps because of it, there is a wide range of hikes, opinions and views on how to use Redux. Many of these approaches are widely at variance with the concepts and examples from the documentation.


At the same time, there are continuing complaints about how Redux “forces” you to do things in certain ways. Many of these complaints actually include concepts related to how Redux is commonly used, and not the actual restrictions imposed by the Redux library itself. (For example, only in one recent HN thread I saw complaints: “too much template code”, “action constants and action creators are not needed”, “I have to edit too many files to add one feature”, “why do I Should I switch between files to get to my logic? "," Terms and names are too complicated to learn or confusing, "and too many others.)


As I explored, read, discussed, and studied the variety of ways to use Redux and the ideas shared in the community, I came to the conclusion that it is important to distinguish between how Redux actually works, conceived ways of its conceptual use, and almost infinite number of ways to use Redux. I would like to touch upon several aspects of using Redux and discuss how they fit into these categories. In general, I hope to explain why there are specific patterns and practices of using Redux, Redux's philosophy and design, and what I consider to be “idiomatic” and “non-idiomatic” using Redux.


This post will be divided into two parts. In "Part 1 - Implementation and Design", we will look at the actual implementation of Redux, what specific restrictions it imposes, and why these restrictions exist. Then, we will consider the initial design and design goals for Redux, based on the discussions and statements of the authors (especially at the early stage of the development process).


In “Part 2 - Practice and Philosophy” we explore common practices that are widely used in Redux applications, and describe why these practices exist in the first place. Finally, we will look at a number of “alternative” approaches to using Redux and discuss why many of them are possible , but not necessarily “idiomatic . ”


The Basics


Learning the Three Principles


Let's start with a look at the now famous Three Principles of Redux



In a very real sense, each of these statements is a lie! (or, borrowing a classic replica from Return to DJ, "they are true ... from a certain point of view." )



But if these statements are not completely true, why are they needed at all? These principles are not fixed rules or literal statements about the implementation of Redux . Rather, they form a statement about the concept of how Redux should be used.


This topic will continue in the rest of the current discussion. Due to the fact that Redux is such a minimal library in terms of implementation, it requires so little or imposes on a technical level. This raises a valuable side discussion that is worth a look.


"Language" or "Meta-language"


In his speech on ReactConf 2017 “Taming Meta-Language”, Cheng Lu describes that only the source code is “language”, and everything else, like comments, tests, documentation, tutorials, blog posts, and conferences, is “meta-language” . In other words, the source code itself can transmit only a certain piece of information. Many additional layers of information transfer at the person level require that people understand “language”.


Further, Cheng Lu continues to discuss how the displacement of additional concepts into the language itself allows us to express more information through the medium of the source code, without resorting to the use of a “meta-language” to convey ideas. From this point of view, Redux is a tiny “language” and almost all the information about how to use it is actually a “meta-language” .


The “language” (in this case, the main Redux library) has minimal expressiveness, and therefore the concepts, norms and ideas surrounding Redux are all at the “meta-language” level. (In fact, the post Understanding "Taming Meta-Language" , which lists the ideas from Cheng Lu's speech, calls Redux a concrete example of these ideas.) Ultimately, this means understanding why certain practices exist around Redux, and decisions about what is and is not “idiomatic” will include opinions and discussions, not just a definition based on the source code .


How Redux really works


Before we go deep into the philosophical side of things, it is important to understand what technical expectations Redux really has. A look at the insides and implementation will be informative.


Redux kernel: createStore


The createStore function is the central part of the Redux functionality. If we cut off comments, error checking, and code for a couple of advanced features, such as store enhancers (storage amplifiers — features that enhance the capabilities of the store — translator's note) and observables, this is what createStore looks like (sample code borrowed from “build mini Redux” »Tutorial called " Hacking Redux " ):


function createStore(reducer) { var state; var listeners = [] function getState() { return state } function subscribe(listener) { listeners.push(listener) return unsubscribe() { var index = listeners.indexOf(listener) listeners.splice(index, 1) } } function dispatch(action) { state = reducer(state, action) listeners.forEach(listener => listener()) } dispatch({}) return { dispatch, subscribe, getState } } 

This is about 25 lines of code, but they still include key functionality. The code keeps track of the current state value and the set of subscribers, updates the value and notifies subscribers when action is dispatched, and provides an API for the store.


Take a look at all the things that this fragment does not include :



In this vein, it is worth quoting Dan Abramov's pull-request for an example of a “classic counter”:


A new example of “classic counter” aims to dispel the myth that Redux requires Webpack, React, hot reboot, sagas, action creators, constants, Babel, npm, CSS modules, decorators, excellent knowledge of Latin, subscriptions to Egghead , Degree, or S.O.V. No, this is just HTML, some handicraft script tags and good old DOM manipulations. Enjoy!

The dispatch function inside createStore simply calls the function-reducer and stores any return value . And yet, despite this, the elements in that list of ideas are widely regarded as concepts that a good application on Redux should take care of.


Having explained all those things to which createStore does not matter, it is important to note that this function actually requires. This createStore function imposes two specific restrictions: actions that go to the store must be simple objects , and actions must have a "type" field not equal to undefined .


Both of these limitations stem from the original concept of "Flux architecture." Quoting the Flux Actions section and Dispatcher from the Flux documentation:


When new data enters the system, both through the person interacting with the application and through the web api call, this data is packaged into an action object containing the new data fields and the specific action type. We often create a library of helper methods called ActionCreators that not only create an action object, but also pass an action to the dispatcher. Different actions are identified by the “type” attribute. When all the stores get an action, they usually use this attribute to determine if they should react to it and how. In the application, Flux, stor's and view control themselves; they are not affected by external objects. Actions come to stor through callback functions that they define and register, and not by installation methods (setters).

Initially, Redux did not require a special “type” field, but later a validation check was added to help catch possible typos or incorrect import of action constants, and to avoid useless arguments about the basic structure of objects.


Built-in utility: combineReducers


Here we begin to observe some limitations familiar to more people. combineReducers expects that every cutoff reducer passed into it will “correctly” respond to an unknown action, returning its default state and never return undefined. She also expects that the value of the current state is a simple JS object, and that there is an exact correspondence between the keys in the current state object and in the object of the-reducer function. Finally, combineReducers checks for equality by reference, to determine if all the cutoff reducer has returned to its previous value. If all returned values ​​look like previous values, combineReducers assumes that nothing has changed anywhere and, as an optimization, returns the original root state object.


Primary Advantage: Redux Developer Tools (DevTools)


The Redux developer’s tools consist of two main parts: an enhancer for the store, which implements time travel by tracking disputed actions, and a user interface that allows you to view and manage history. By itself, the store enhancer does not care about the contents of action or state, it simply stores action in memory. Initially, the developer tools interface had to be rendered inside the component tree of your application, and it also did not care about the content of the action or state. However, the Redux DevTools extension works in a separate process (at least in Chrome), and therefore requires the serializability of all actions and states, so that all possibilities of moving in time work correctly and quickly. The ability to import and export state and action also requires that they be serializable.


Another semi-demanding requirement for debugging is through immobility and clean functions. If the function-reducer mutates the state, then the transition between the actons in the debugger will result in inconsistent values. If the reducer has side effects, then these side effects will occur every time DevTools repeats action. In both cases, debugging by moving in time will not work completely as expected.


Main UI Bundles: React-Redux and connect


The mutation becomes a real problem in the connect function from React-Redux. The wrapper components generated by connect implement many optimizations to ensure that the wrapped components are rendered only when they are actually needed. These optimizations revolve around reference equality tests to determine if the data has changed.


In particular, every time action is dispatched and subscribers are notified, connect checks if the root state object has changed. If not, connect assumes that nothing has changed in the state, and skips further rendering work (That's why combineReducers tries, as far as possible, to return the same root state object). If the root state object has changed , connect will call the provided function mapStateToProps, and perform a shallow test for equality between the current result and the previous one, to determine if the props calculated from the store data has changed. Again, if the content of the data looks the same, connect will not render the wrapped component. These equality checks in connect are the reason why random state mutations do not lead to component re-rendering, this is because connect assumes that the data has not changed and no re-rendering is needed.


Related Libraries: React and Reselect


Immunity also matters in other libraries, often used in conjunction with Redux. The Reselect library creates memory selector functions commonly used to extract data from the Redux state tree. Memorization of values, as a rule, relies on the check of referential equality, to determine whether the input parameters coincide with those previously used.


Also, although the React component can implement shouldComponentUpdate using whatever logic it wants, the most common implementation relies on shallow checks of the equality of current props and new incoming props, for example:


 return !shallowEqual(this.props, nextProps) 

In any case, data mutation usually leads to undesirable behavior. Memorizing selector functions will not return the correct values, and React's optimized components will not be rendered when they should.


Summing up Redux technical requirements


The central function of Redux createStore in itself imposes only two restrictions on how you should write your code: actions should be simple objects, and must contain a certain type. It does not care about immunity, serializability, side effects, or what type field actually takes on value.


With this in mind, widely used parts around this kernel, including Redux DevTools, React-Redux, React, and Reselect, really rely on the proper use of immunity, serializability of action / state, and pure redundancy functions . The main logic of the application can work normally if these expectations are ignored, but, with high probability, debugging by time movement and re-rendering of components will break. They will also affect any other uses associated with consistency.


It is important to note that immobility, serializability and pure functions are not imposed in any way by Redux. The function-reducer may well mutate its state or make an AJAX call. Any other part of the application may well call getState () and modify the contents of the state tree directly. It is fully possible to place promises, functions, symbols, class instances, or other non-serializable values ​​in action or state tree. You should not do any of this, but it is possible .


Redux Design and Design


Keeping in mind these technical limitations, we can pay attention to how the use of Redux is conceived. To better understand this thought, it is helpful to look at the ideas that led to the initial development of Redux.


Impact on Redux and its goals


The Introduction section in the Redux documentation contains several basic ideas that influenced the development and concepts of Redux, in the themes Motivation, Key Concepts, and Predecessors . As a brief summary:



README Redux'a.



  • , Redux.
  • (Stores, Action Creator', ) «» .
  • Flux', .
  • - Flux.
  • , , «» .
  • : JS , , ImmutableJS ..
  • , Redux , .
  • , Stor'.
  • (, , /)
  • , Redux.
  • stor' action'. — .
  • (mocks).
  • «» Stor', Stor' .
  • API .
  • «» ?



Redux, issue-, , Redux.


Redux Flux


Redux « » Flux. Flux: « (dispatching) action'», action' — type, « » action' (action creators), , « » , .


« Redux », : « Flux' Flux ».



Redux', , . , .


, action, , , action ( ). store , , action , .


« » « » , .


action'


Redux' type action', , action' . Redux DevTools type action', , .


, , . , action . , , action , action . action (, SET_DATA), action , .


Redux


Redux , , , . , : , , .


, Redux , , , , «» «». ( , Redux' , , : — .)


Redux


reducer' , , -reducer . reducer' — AJAX .


AJAX - - , , , - . , .


-reducer'


Redux «» Flux store. Flux Redux «» Flux . Flux UsersStore, PostsStore CommentsStore, Redux , : { users, posts, comments }.


, , . , . , «reducer » , , , . « reducer'» . combineReducers Redux' .


-reducer , , action' reducer' , . , action' reducer' , Redux'. : «action' 1-- reducer'.»



Redux «». ( applyMiddleware store enhancers) , , , .


Redux . -reducer, . combineReducers , — reducer' . reducer' . , reducer' , — , .


Flux', Stor' waitFor(), . CommentsStore PostsStore , , PostsStore.waitFor(), , PostsStore . , . , Redux -reducer' .


( ) «Combining Stateless Stores»


, commentsReducer action'. hasCommentReallyBeenAdded (). API. , « », : reducer . store . , , - .

 export default function commentsReducer(state = initialState, action, hasPostReallyBeenAdded) {} //    export default function rootReducer(state = initialState, action) { const postState = postsReducer(state.post, action); const {hasPostReallyBeenAdded} = postState; const commentState = commentsReducer(state.comments, action, hasPostReallyBeenAdded); return { post : postState, comments : commentState }; } 

«reducer' ». reducer reducer', / .


API Redux'


Redux'. :


Andrew — #195:


API — API. middleware stor' , Redux — dispatch() createStore() . , 1.0. . , API.

Dan — #216:


Redux NuclearJS:
  • ImmutableJS
  • API
  • , Redux -

Redux , .


API createStore, . , (Reducer, Action Creator) - Redux. , Redux , , .


Redux ,


« API». Flux, Flummox , , (, START/SUCCESS/FAILURE action' ). - , , .


Hashnode AMA (/) :


Andrew — #55:


action' , action middleware, middlewar', , .

Andrew — #215:


, , Redux , , , , Flummox. , .

( … Slack) Ko Flux . , , «» , reduxjs Github'.


Dan — Hashnode AMA:


- Redux', , Rx . , , Redux Rx, middlewar'.

Andrew — Hashnode AMA:


, middleware API , , . Flux — Flummox — , , -middleware. - , - , , . Redux , , , . Redux Thunk , . , / . !


. , Redux , . README, Redux' , , API . , Redux , Redux , .


Redux', 2 — , , Redux, «» , Redux.




A source


, , .


')

Source: https://habr.com/ru/post/330692/


All Articles