Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Redux - Modeling state of a complex graph and triggering multiple updates and side-effects in response to a single action

Problem

I am trying to design webapp with a fairly complex state, where many single actions should trigger multiple changes and updates across numerous components, including fetching and displaying data asynchronously from external endpoints.

Some context and background:

I am building a hybrid cytoscape.js / redux app for modeling protein interactions using graphs.

My Redux store needs to hold a representation of the graph (a collection of node and edge objects), as well as numerous filtering parameters that can be set by the user (i.e only display nodes that contain a certain value, etc).

The current implementation uses React.js to manage all the state and as the app grew it became quite monolithic, hard to reason about and debug.

Considerations and questions

Having never used Redux before , I'm struggling a bit when trying to conceptually design the new implementation. Specifically, I have the following questions / concerns:

  1. Cytoscape.js is an isolated component since it's directly manipulating the DOM. It expects the state (specifically the node and edge collections) to be of a certain shape, which is nested and a little hard to handle. Since every update to any node or edge object should be reflected graphically in cytoscape, should I mirror the shape it expects in my Redux store, or should I transform it every time I make an update? If so, what would be a good place to do it? mapStateToProps or inside a reducer?

  2. Certain events, such as selecting nodes and/or edges, crate multiple side-effects across the entire app (data is fetched asynchronously from the server, other data is extracted from the selection and is transformed / aggregated, some of it derived and some of it from external api calls). I'm having trouble wrapping my head around how I should handle these changes. More specifically, let's say a SELECTION_CHANGE action is fired. Should it contain the selected objects, or just their IDs? I'm guessing IDs would be less taxing from a performance point. More importantly, how should I handle the cascade of updates the SELECTION_CHANGE actions requires? A single SELECTION_CHANGE action should trigger changes in multiple parts of the UI and state tree. Meaning triggering multiple actions across different reducers. What would be a good way to batch / queue / trigger multiple actions depending on SELECTION_CHANGE?

  3. The user needs to be able to filter and manipulate the graph according to arbitrary predicates. More specifically, he should be able to permanently delete \ add nodes and edges, and also restrict the view to a particular subset of the graph. In other words, some changes are permanent (deleting \ adding or otherwise editing the graph) while others relate only to what is shown (for example, showing only nodes with expression levels above a certain threshold, etc). Should I keep a separate, "filtered" copy of the graph in my state tree, or should I calculate it on the fly for every change in the filtering parameters? And as before, if so, where would be a good place to perform these filtering actions: mapStateToProps, reducers or someplace else I haven't thought of?

I'm hoping these high-level and abstract questions are descriptive enough of what I'm trying to achieve, and if not I'll be happy to elaborate.

like image 388
airbag Avatar asked Oct 12 '16 18:10

airbag


1 Answers

The recommended approach to Redux state shape is to keep your state as minimal as possible, and derive data from that as needed (usually in selector functions, which can be called in a component's mapState and in other locations such as thunk action creators or sagas). For nested/relational data, it works best if you store it in a normalized structure, and denormalize it as needed.

While what you put into your actions is up to you, I generally prefer to keep them fairly minimal. That means doing lookups of necessary items and their IDs in an action creator, and then looking up the data and doing necessary work in a reducer. As for the reducer handling, there's several ways to approach it. If you're going with a "combined slice reducers" approach, the combineReducers utility will give each slice reducer a chance to respond to a given action, and update its own slice as needed. You can also write more complex reducers that operate at a higher level in the state tree, and do all the nested update logic yourself as needed (this is more common if you're using a "feature folder"-type project structure). Either way, you should be able to do all your updating for a single logical operation with one dispatched action, although at times you may want to dispatch multiple consecutive actions in a row to perform a higher-level operation (such as UPDATE_ITEM -> APPLY_EDITS -> CLOSE_MODAL to handle clicking the "OK" button on an editing popup window).

I'd encourage you to read through the Redux docs, as they address many of these topics, and point to other relevant information. In particular, you should read the new Structuring Reducers section. Be sure to read through the articles linked in the "Prerequisite Concepts" page. The Redux FAQ also points to a great deal of relevant info, particularly the Reducers, Organizing State, Code Structure, and Performance categories.

Finally, a couple other relevant links. I keep a big list of links to high-quality tutorials and articles on React, Redux, and related topics, at https://github.com/markerikson/react-redux-links . Lots of useful info linked from there. I also am a big fan of the Redux-ORM library, which provides a very nice abstraction layer over managing normalized data in your Redux store without trying to change what makes Redux special.

like image 89
markerikson Avatar answered Oct 23 '22 09:10

markerikson