Handling children state in an arbitrary JSON tree - reactjs

This is more of a brainstorming question as I can't really seem to come up with a good solution.
I have a component which renders a tree based on some passed JSON (stored at the top level). Each node of the tree can have 0..n children and maps to a component defined by the JSON of that node (can be basically anything is the idea). The following is just an example and the names don't mean anything specific. Don't pay too much attention to the names and why a UserList might have children that could be anything.
JSON: {
data: {}
children: [
{
data: {}
children: []
},
{
data: {}
children: []
},
{
data: {}
children: [
{
data: {}
children: []
},
...etc
]
},
]
}
const findComponent = (props) => {
if (props.data.name === "userSelector") {
return <UserSelectorNode {...props}>;
} else if (props.data.name === "userInformation") {
return <UserInformationNode{...props}>; // example of what might be under a userSelectorNode
}
...etc
};
// render a user selector and children
const UserSelectorNode = (props) => {
const [selected, setSelected] = React.useState([])
// other methods which can update the JSON when selected changes...
return (
<div>
<UserSelector selected={selected}/> // does a getUser() server request internally
<div>
{props.data.children.map((child) => findComponent(child))}
<div>
</div>
);
};
This tree can be modified at any level (add/remove/edit). Adding/Editing is easy. The problem is remove operations.
Some children components use existing components which do things like getting a list of users and displaying them in a list (stored in state I have no access to). When a node on the tree is removed new components are made for every node that has to shift (JSON at the index is now different), which can be a lot. This causes a bunch of requests to occur again and sometimes state can be lost entirely (say the page number of a table to view users).
I'm pretty sure there is no way for the "new" UserSelector created when the JSON shifts to keep the same state, but I figured I may as well ask if anyone has dealt with anything similar and how they went about designing it.
The only way I can think of is to not actually reuse any components and re implement them with state stored somewhere else (which would suck), or rewrite everything to be able to take internal state as well as an external state storage if required.
EDIT: Added Sandbox
https://codesandbox.io/s/focused-thunder-neyxf. Threw it together pretty quick to only get a single layer of remove working which shows the problem.

Related

How to update object properties in state array

So, I'm just going through a React course. Learning this framework for the first time so this is likely a dumb question. Here's code I've been given for updating a property on an object stored in state as an array of objects.
const [squares, setSquares] = React.useState(boxes)
function toggle(clickedSquare) {
setSquares(prevSquares => {
return prevSquares.map((square) => {
return square === clickedSquare ? {...square, on: !square.on} : square
})
})
}
...but, the following code I wrote works too and seems simpler, what's wrong with this approach? State values are only shallow immutable. Objects stored in a state array are themselves mutable, as far as I can tell...
const [squares, setSquares] = React.useState(boxes)
function toggle(clickedSquare) {
clickedSquare.on = !clickedSquare.on;
setSquares(prevSquares => [...prevSquares])
}
Also consider this example where I have an array containing deeply nested objects held in state.
[
{
trunk: {
limb: {
branch: {
twig: {
leaf: {
color: "green"
}
}
}
}
}
}
]
I want to change that "green" to "brown". It states in this article Handling State in React that...
Deep cloning is expensive
Deep cloning is typically wasteful (instead, only clone what has actually changed)
Deep cloning causes unnecessary renders since React thinks everything has changed when in fact perhaps only a specific child object has changed.
The thing that has changed in the tree example is just the leaf object. So only that needs to be cloned, not the array and not the whole tree or trunk object. This makes a lot more sense to me. Does anyone disagree?
This still leaves (no pun intended) the question of what bugs can be introduced by updating property values in a state array my way and not cloning the object that has the change? A single concrete example would be very nice just so I can understand better where I can optimize for performance.
clickedSquare.on = !clickedSquare.on; is a state mutation. Don't mutate React state.
The reason the following code is likely working is because it has shallow copied the squares state array which triggers a rerender and exposes the mutated array elements.
function toggle(clickedSquare) {
clickedSquare.on = !clickedSquare.on; // <-- mutation!
setSquares(prevSquares => [...prevSquares]); // new array for Reconciliation
}
It may not have any adverse effects in this specific scenario, but mutating state is a good foot gun and likely to cause potentially difficult bugs to debug/diagnose, especially if their effects aren't seen until several children deeper in the ReactTree.
Just always use the first method and apply the Immutable Update pattern. When updating any part of React state, even nested state, new array and object references need to be created for React's Reconciliation process to work correctly.
function toggle(clickedSquare) {
setSquares(prevSquares => prevSquares.map((square) => // <-- new array
square === clickedSquare
? { ...square, on: !square.on } // <-- new object
: square
));
}
Here, You are using map() method which return only true or false for that condition.
So, You should use filter() method instead of map() method which return filtered data for that condition.
For Example:
const arr = [
{
name: 'yes',
age: 45
},
{
nmae: 'no',
age: 15
}
]
const filterByMap = arr.map(elm => elm.age > 18)
console.log(filterByMap) // outputs --> [ true, false ]
const filterByFilter = arr.filter(elm => elm.age > 18)
console.log(filterByFilter) // outputs --> [ { name: 'yes', age: 45 } ]

How to avoid reinstancing class instances returning from Redux on React

The problem
Currently, we are dissatisfied with our company's app performance and we want to enhance it by refactoring. I'm not going to go in detail on how it was but how we agree now it should be. The app has many moving parts and nested instances which all play a role in rendering what we are building, these instances are just pure js classes with relevant methods. One such example, from the older version is:
export default class Gate {
readonly id: string;
name?: string;
color?: string | null;
xAxis: string;
yAxis: string;
parents: Gate[] = [];
children: Gate[] = [];
isPointInside(point: { x: number; y: number }): boolean {
for (const parent of this.parents) {
if (!parent.isPointInside(point)) {
return false;
}
}
return true;
}
}
...
export default class PolygonGate extends Gate {
points: { x: number, y: number }[] = [];
isPointInside(point: { x: number; y: number }): boolean {
return pointInsidePolygon(point, this.points) &&
super.isPointInside(point);
}
}
As you can see, the "isPointInside()" function takes a lot of care on how it's handled, and it depends on careful relation of class instances.
On our refactor, we want to rely as much as possible on React for managing state changes. That would mean we would use Redux for changes, basically any point in the app that calls a change can commit it. If I add a new Gate or change a gate's points, as explained in the example above, that would propagate down in the hierarchy from a top level component (Workspace) as props to others components which would render it.
const Workspace = () => {
const store = useStore();
const [workspace, setWorkspace] = React.useState(store.getState().workspace);
useEffect(() => { setWorkspace(store.getState()) }, [store.getState])
...
}
This where the problem begins: we can't store class instances in Redux, therefore it would be plain JS objects.
Proposed solutions
The first and easiest solution is to just re-instance every object received from Redux:
const Workspace = () => {
const store = useStore();
const [workspace, setWorkspace] = React.useState(store.getState().workspace);
useEffect(() => { setWorkspace(store.getState()) }, [store.getState])
...
const gates = workspace.gates.map(e => new Gate(e));
}
But this would mean every change would cause a huge number of objects to re-instanced. Needless to say, very expensive and we would have to manage links from one object to another.
Second option was to turn it all in purely functional components:
// Instead of this
// gate.isPointInside(point)
// We would have this
isPointInside(gate, point)
// gate === pure js object
The comes with an associated host of problems. Firstly we would have to store everything on redux as references. For example, this could the structure:
{
"plots": [
{
// Plot data...
"file": "a1b2c3-d4e5f6",
"gates": [
"010203-a4a5a6"
]
}
],
"gates": [
{
// Gate data...
"id": "010203-a4a5a6"
}
],
"files": [
{
// File data...
"id": "a1b2c3-d4e5f6"
}
]
}
Which just sounds like a source of many potential problems caused by improperly managed dependencies between objects. Again, we would have to manage links from one object to another every single re-render.
Other solutions discussed was having an object pool which we could use, but that comes with it's own headaches and so on. Another option was to avoid Redux and self-implement a solution, but that sounds like like massive and buggy work, and would kinda defeat the purpose for the whole reason we considered Redux in the first place.
I would like to know from you, what do you think is the best approach here?

Accessing a List of Objects when your State is Normalized / Flat

Redux recommends your state be flat per here: https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape
So say my state was like this:
{
selectedPlaylistIndex: 0,
pageDict: {},
playlistDict: {},
playlistList: [] // holds IDs from playlistDict
}
a sample Playlist Object would look like:
{
id: someId,
active: false,
pageList: [], // holds IDs from pageDict
}
If I want to state create a Container for displaying the "pageList" of a Playlist object, I pass in the Playlists' "pageList" property as a list of the full Page objects (as opposed to the IDs). I feel like it's an expensive operation as anytime pageDict, playlistDict, playlistList, or selectedPlaylistIndex get updated, it will be rendered and the function will run.
Is there a more elegant / better way of doing this? I feel like I'm missing something.
// Expensive Operation; Want to Find Better Solution?
getSelectedPlaylistPageObjArr() {
const { selectedPlaylistIndex, pageDict, playlistDict, playlistList } = this.props;
return playlistDict[ playlistList[ selectedPlaylistIndex ]].pageList.map( id => pageDict[id] ) : [];
}
render() {
return (
<Playlist
pageObjArr={this.getSelectedPlaylistPageObjArr()}
/>
);
}
const mapStateToProps = ( state ) => {
return {
pageDict: state.entities.pageDict,
playlistDict: state.entities.playlistDict,
playlistList: state.entities.playlistList,
selectedPlaylistIndex: state.application.selectedPlaylistIndex,
};
};
Generalising, you're asking how to handle relational data.
Last time I had to deal with relational data I integrated redux-orm library. It's a small and immutable ORM to manage relational data in your Redux store.
So let's say your business logic is as follows 1 Playlist has many Pages, then in order to get the Pages of the selected Playlist (by playlist's id), it would be computed with redux-orm as follow:
const getSelectedPlaylistId = state => state.application.selectedPlaylistIndex
const getPlaylistPages = createSelector(
orm,
getSelectedPlaylistId,
({ Playlist }, playlistId) => {
return Playlist.withId(playlistId).Pages.all().toRefArray();
}
);
Once you invoked getPlaylistPages the result will be cached and recalculated only when one of the accessed models are changed.
Also if you don't want to use redux-orm (let's assume your app doesn't have a lot of models or any other reason), then you can just use reselect library, that will cache your performance cost computations.
As I already mention I had such an experience, and my thoughts and conclusions are summarized in the following SO question: How to deal with relational data in Redux?

What's the proper way to grab an object from my Entities dictionary in a Normalized state and pass it to a component?

I have a state that looks like:
entities: {
pageDict: {
['someId1']: { name: 'page1', id: 'someId1' },
['someId2']: { name: 'page2', id: 'someId2' },
['someId3']: { name: 'page3', id: 'someId3' }
}
}
lists: {
pageIdList: ['someId1', 'someId2', 'someId3']
}
And a Component that looks like:
const Pages = ( props ) => {
return (
<div>
props.pageIdList.map(( pageId, key ) => {
return (
<Page
key={ key }
pageObj={ somethingHere } // What to do here?
/>
);
})
</div>
);
}
To grab the object, I would need to do:
let pageObj = state.entities.pageDict[ pageId ];
I guess I can pass the state.entities.pageDict as props from the Containers.. but I'm trying to look at the selectors pattern and looking at this:
https://redux.js.org/docs/recipes/ComputingDerivedData.html
and I'm wondering if I'm doing this wrong, can someone give me some insight? I just want to make sure I'm doing this correctly.
EDIT: I got the idea of using Entities from the https://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html and the SoundCloud Redux project
EDIT2: I'm looking at things online like this https://github.com/reactjs/redux/issues/584 but I'm questioning if I'm using this incorrectly and I'm not sure how to apply this to my own project
EDIT3: I'm leaning on creating a selector that will get the pageIdList and return a list of the pageobjects from the pageDict.. that way I already have the object to pass into my Page component.
I think I follow what you're trying to do here. With Redux try thinking of your user interface as always displaying something immutable: rather than "passing something to it" it is "reading something from the state". That when when your state changes your user interface is updated (it isn't always this simple but it is a pretty good start).
If I read your answer correctly you have a Map of pages:
//data
{
id1: {...pageProperties} },
id2: {...pageProperties} },
id3: {...pageProperties} },
}
and your page list is the order these are displayed in:
ex:
[id2, id3, id1]
Your page object might look something like this:
//Page.js
class Page extends React.Component {
render() {
const { pageIdList, pageEntities } = this.props //note I'm using props because this is connected via redux
return (
<div>
{ pageIdList.map((pageId, index)=>{
return <Page key={index} pageEntity={pageEntities[pageId]} /> //I'm just pulling the object from the map here
}}
</div>
)
}
}
//connect is the Redux connect function
export default connect((store)=> { //most examples use "state" here, but it is the redux store
return {
pageEntities: store.pageEntities,
pageIdList: store.pageList
}
})(Page)
Now when we want to change something you update the state via a dispatch / action. That is reduced in the reducer to display the new state. There are a lot of example out there on how this works but the general idea is update the state and the components take care of displaying it.
The result of this code (and there might be some typeos) is that you should see:
Page id: 2, Page id: 3, Page id: 1 because the list of the pages in the state is 2, 3, 1 in the example I gave.
To answer your question specifically what the entity I'm pulling is the global Redux Store (not the internal component state). 'Map State to Props' is really 'Map Store to Props' as the 'state' is part of React and the 'store' is your Redux store.
Does that help? React+Redux is really nice once you figure it out, it took me a solid month to understand all the ins and outs but it really does simplify things in the long run.

React Parent-Child Relationship and Encapsulation

I have a React design problem that I am trying to solve, and I am running into an issue counter-intuitive to the idea of encapsulation as I try to breakdown an existing component into parent-child to support an additional use case. I am using React 15.3.2 with Redux, ES6, and ImmutableJS. First, I will illustrate the design problem, then I will provide snippets to illustrate why I feel that I have the need to get data back from children and how that is good for encapsulation, and the roadblock I am hitting.
I have read this stackoverflow which has detailed explanation on why passing data from children to parent component is not a good idea,
Pass props to parent component in React.js
But I have some concerns, which I will discuss at the end.
Design:
I am starting with a CheckboxSelect component. The Title bar's text depends on the checked items.
Closed:
Open with selections (current implementation):
To support additional use-case, the dropdown will now open up with more stuff.
Open with selections (new update):
Initial Code:
I am starting with a CheckboxSelect controlled component with the following props interface:
CheckboxSelect.jsx:
CheckboxSelect.propTypes = {
// what title to display by default with no selection
defaultTitle: PropTypes.string.isRequired, ie. "Selected Scopes"
// array of selected options, ie. [{key: "comedy", name: "comedy", checked: false }, ...]
options: PropTypes.array.isRequired,
// handler for when the user checks a selection, this will update
// the reducer state, which causes the options prop to be refreshed and
// passed in from the outer container
onCheck: PropTypes.func.isRequired,
onUncheck: PropTypes.func.isRequired,
onCheckAll: PropTypes.func,
onUncheckAll: PropTypes.func,
className: PropTypes.string,
// controls the open state of the dropdown
open: PropTypes.bool,
// handler for when the user clicks on the dropdown button, this will update the reducer state,
// which causes the open prop to be refreshed and passed in from the outer container
onClick: PropTypes.func,
onCancel: PropTypes.func,
};
// there is currently some logic inside the component to generate the title to display
// produces "comedy, action"
getTitle() {
const { defaultTitle } = this.props;
const checked = this.getChecked();
let fullTitle;
if (this.allChecked() || this.allUnchecked()) {
fullTitle = `${defaultTitle } (${checked.length})`;
} else {
fullTitle = checked.map((option) => option.name).join(', ');
}
return fullTitle;
}
getChecked() {
const { options } = this.props;
return options.filter(option => option.checked);
}
allChecked() {
const { options } = this.props;
return this.getChecked().length === options.length;
}
allUnchecked() {
return this.getChecked().length === 0;
}
ApplicationContainer.jsx (where the component is being used):
scopeDropDownOptions = (currentMovie) => {
// returns [{key: "comedy", name: "comedy", checked: false }]
const applicableScopes = currentMovie.getIn(['attributes', 'applicable_scopes']);
return currentMovie.getIn(['attributes', 'available_scopes']).reduce((result, scope) => {
result.push({
key: scope,
name: scope,
checked: (applicableScopes.includes(scope)),
});
return result;
}, []);
}
onSelectScope = (scope) => {
const movieScopes = this.applicableScopes.push(scope.key);
this.onUpdateField('applicable_scopes', movieScopes);
}
render() {
...
<CheckboxSelect
defaultTitle="Selected Scopes"
options={this.scopeDropdownOptions(currentMovie)}
onCheck={this.onSelectScope}
onUncheck={this.onDeselectScope}
onCheckAll={this.onSelectAllScopes}
onUncheckAll={this.onDeselectAllScopes}
open={store.get('scopeDropdownOpen')}
</CheckboxSelect>
}
New Code:
To support the new layout, I would like to break the existing component into two: a DynamicDropdown that contains CheckboxSelect2 as one of the children, along with any other elements that may be dropped down. Here is how the new code will look inside the ApplicationContainer.
ApplicationContainer.jsx
scopeDropDownOptions = (currentMovie) => {
// returns [{key: "comedy", name: "comedy", checked: false }]
const applicableScopes = currentMovie.getIn(['attributes', 'applicable_scopes']);
return currentMovie.getIn(['attributes', 'available_scopes']).reduce((result, scope) => {
result.push({
key: scope,
name: scope,
checked: (applicableScopes.includes(scope)),
});
return result;
}, []);
}
onSelectScope = (scope) => {
const {store } = this.props;
const cachedApplicableScopes = store.get('cachedApplicableScopes').push(scope.key);
store.get('cachedApplicableScopes').push(scope.key);
this.actions.setCachedApplicableScopes(cachedApplicableScopes);
// wait until apply is clicked before update
}
render() {
...
<DynamicDropdown
className="scope-select"
title={this.scopeDropdownTitle()}
open={store.get('scopeDropdownOpen')}
onClick={this.onScopeDropdownClick}
onCancel={this.onScopeDropdownCancel}
>
<CheckboxSelect2
options={this.scopeDropdownOptions(currentMovie)}
onCheck={this.onSelectScope}
onUncheck={this.onDeselectScope}
onCheckAll={this.onSelectAllScopes}
onUncheckAll={this.onDeselectAllScopes}
visble={store.get('scopeDropdownOpen')}
/>
// ... other children like confirmation message and buttons
</DynamicDropdown>
}
// logic related to CheckboxSelect2 title generation moved to the ApplicationContainer. Not ideal in my opinion as it breaks encapsulation. Further discussions in the Choices section
getScopesChecked() {
const options = this.scopeDropdownOptions(this.currentMovie);
return options.filter(option => option.checked);
}
scopesAllChecked() {
const options = this.scopeDropdownOptions(this.currentMovie);
return this.getScopesChecked().length === options.length;
}
scopesAllUnchecked() {
return this.getScopesChecked().length === 0;
}
scopeDropdownTitle() {
const defaultTitle = "Selected Scopes";
const checked = this.getScopesChecked();
let fullTitle;
if (this.scopesAllChecked() || this.scopesAllUnchecked()) {
fullTitle = `${defaultTitle} (${checked.length})`;
} else {
fullTitle = checked.map((option) => option.name).join(', ');
}
return fullTitle;
}
Problem:
The problem I have is with populating the title props of the DynamicDropdown element with the New Code, since it depends on the result of the CheckboxSelect2 selection.
Keep in mind CheckboxSelect2 is a dumb controlled component, we pass an onCheck handler to it. The this.onSelectScope inside the ApplicationContainer, is responsible for updating the reducer state of what has been checked, which in turn refresh the props and causes the DynamicDropdown and CheckboxSelect2 to be re-rendered.
Choices:
In the old code, there is a group of logic used to figure out the title to display for the dropdown. Here are the choices I am presented with:
To keep encapsulation of letting the CheckboxSelect2 summarize the
title, I tried initially keeping the same title logic inside
CheckboxSelect2, and accessing it via a ref.
ApplicationContainer.jsx
<DynamicDropdown
title={this.childCheckboxSelect.getTitle()}
>
<CheckboxSelect2
ref={(childCheckboxSelect) => this.childCheckboxSelect = childCheckboxSelect}
>
</DynamicDropdown>
At the time that DynamicDropdown is re-rendered, CheckboxSelect2
hasn't received the new props yet from the parent via the one-way
data-flow, so as a child, it actually cannot generate the most
up-to-date title for the DynamicDropdown based on what has been
checked. With this implementation, my title is one-step behind what
was actually checked.
As shown in the ApplicationContainer.jsx for the New Code section
above, the logic for the scopeDropdownTitle could be moved out from
CheckboxSelect2 to ApplicationContainer.jsx, so it sits a level
above DynamicDropdown, enabling the DynamicDropdown to get the
updated title from the reducer state as it renders. While this
solution works, it totally breaks my view on encapsulation, where
the responsibility for determining what title to be displayed, is
something that the CheckboxSelect2 should know about. Basically the
title logic in ApplicationContainer.jsx, now also pre-generates the
options props meant to passed to CheckboxSelect2 to render that
component, the CheckboxSelect2 logic is bleeding into the outer
container.
Let's look at the argument in the stackoverflow post Pass props to parent component in React.js and how it relates to this design problem and an analogy:
"The parent already has that child prop. ...  if the child has a
prop, then it is because its parent provided that prop to the
child!"  Sure, the ApplicationContainer has all the knowledge it
needs to generate the title for the parent DynamicDropdown based on
the checked states of the child CheckboxSelect2, but then is it
really the responsibility of the ApplicationContainer?
Let me give an analogy of a Manager asking an Employee to produce a Report. You can say, the Manager already has all the info, he can surely produce the Report himself! Having a controlled component where the
container manages update to the props for the child via callbacks, seems to me like a Manager passing a bucket to the Employee, the Employee passes back bits and pieces of information in the bucket, and tell the Manager to do the work to summarize things instead of the Employee producing a good summary (which is good encapsulation).
"Additionally, you could have used a ref directly on the child"
I think in the Choices 1) I stated above, using ref does not seem to work when you want up-to-date information from the child as
there is a circular dependency issue with one-way dataflow (ie.
parent needs to get up-to-date summary information from the child,
but the child first depends on the up-to-date information from the
parent).
If you have read this end-to-end and understood the problem, I appreciate your effort! Let me know what you think.

Resources