Recursive component rendering based on a tree menu object - reactjs

I have created a component which adds an additional selection box dropdown whenever a key inside an object is another object.
For example, consider the following object:
{
a1: {
x1: 1,
x2: 2,
x3: 3,
x4: {
z1: "z1",
z2: "z2"
},
x5: [
{
x5a: {
z5a1: 1,
z5a2: 2
}
},
{
x5b: {
z5b1: 1,
z5b2: 2
}
}
]
},
a2: {
x1: 1,
x2: 2,
x3: 3
},
a3: "some values"
};
What I want to achieve is (when I select a value from the dropdown menu):
if subTree[value] is an object ({}) or an array ([]), display its keys or indices in
a new selection box drop down, directly bellow the current
else stop
Initial display
Selecting a value in the dropdown
After I select a value, the next selection will show empty, and so on and so forth...
The problem
When I update a value in a selection box, my code doesn't update/clear the selections bellow it properly.
The source code of my project is available at: https://codesandbox.io/s/frosty-grass-9jdue

When changing a value that is not the last in the path, you need to clear all subsequent selections because they were based on a different path. I'm not quite sure how we do that in your setup because I haven't quite wrapped my head around it.
What makes sense to me is to store the pieces of the path as an array. That way we can use slice to remove the tail. I am going to use lodash's get method as a helper to access the value at a path. I am expecting the prop data to be the object itself rather than Object.entries like you were doing before.
import React, { useState, useEffect } from "react";
import { MenuItem, TextField } from "#material-ui/core";
import _get from "lodash/get";
const InfiniteSelection = ({ data, onCaseCadeSelection }) => {
// an array of segments like ['a1', 'x4', 'z1']
const [path, setPath] = useState([]);
// joins to a string like `a1.x4.z1`
const cascade = path.join(".");
// call callback whenever the cascade changes
useEffect(() => {
if (onCaseCadeSelection) {
onCaseCadeSelection(cascade);
}
}, [cascade]);
// need to know the index in the paths array where the change occurred
const handleChange = (index) => (event) => {
// set this value and delete everything after it
setPath([...path.slice(0, index), event.target.value]);
};
// options for the NEXT value from a given path
const optionsForPath = (path) => {
// lodash get handles this except when path is empty array []
const value = path.length > 0 ? _get(data, path) : data;
// get the options from this path, or null if it is terminal
return typeof value === "object" ? Object.keys(value) : null;
};
// either the current path is to a terminal value, or there should be one more level of selects
const currentOptions = optionsForPath(path);
// helper function can be used as a callback to path.map
// will also be called one extra time for the next values if not on a terminal value
const renderSelect = (value, index) => {
return (
<SelectControlled
className="text form_text"
variant="outlined"
list={optionsForPath(path.slice(0, index)) ?? []}
onChange={handleChange(index)}
value={value ?? ""}
/>
);
};
// render selects for each element in the path and maybe a next select
return (
<div className="vertically_spaced">
{path.map(renderSelect)}
{currentOptions === null || renderSelect("", path.length)}
</div>
);
};
Code Sandbox Link

From #LindaPaiste's answer:
When changing a value that is not the last in the path, you need to clear all subsequent selections because they were based on a different path.
That's the key to solving your problem! You have to somehow blow away and forget everything bellow the selection box whose value you are currently changing.
React was designed around the "blow away and forget" principle. Note also that The Data Flows Down. With that in mind, your task should be fairly easy to complete and while Linda's solution seems to work, it is perhaps not as simple as it could be.
What if we could have a special component that (1) accepts a sub-tree of your data, (2) renders its 1st level children as a selection box dropdown and then (3) repeats the process recursively? Something like this:
<RecursiveComponent subTree={DATA_SAMPLE} {/*maybe some other props*/}/>
When we think of recursion, we have to think of terminal conditions. In our case, this happens when the sub-tree is a primitive type (i.e. not an object ({}) or an array ([])).
Every RecursiveComponent has to:
render the selection menu dropdown, containing all the 1st level children of the sub-tree
render the nested RecursiveComponent, based on props.subTree[selection]
handle user interaction
Something like this:
import { MenuItem, Select } from "#material-ui/core";
import { useState } from "react";
function RecursiveComponent(props) {
const [selection, setSelection] = useState(props.currentSelection);
const handleChange = (event) => {
setSelection(event.target.value);
};
return (
<>
<Select variant="outlined" value={selection} onChange={handleChange}>
{Object.keys(props.subTree).map((key) => (
<MenuItem value={key}>{key}</MenuItem>
))}
</Select>
<div /> {/* forces a line break between selection boxes */}
{props.subTree[selection] !== Object(props.subTree[selection]) ? (
<></>
) : (
<RecursiveComponent
subTree={props.subTree[selection]}
currentSelection=""
/>
)}
</>
);
}
export default RecursiveComponent;
This is how you can use RecursiveComponent in your project by editing index.js:
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { DATA_SAMPLE } from "./DataSample";
import RecursiveComponent from "./RecursiveComponent";
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<RecursiveComponent subTree={DATA_SAMPLE} currentSelection="" />
</StrictMode>,
rootElement
);

Related

Hello, on ReactJs, I loose focus on input when useState change value :

Here an example of the problem :
codesandbox.io
export default function App() {
const [hasInputChanged, setHasInputChanged] = useState(false);
let colorList = ["orange", "blue", "yellow"];
function handleChange(e) {
setHasInputChanged(true);
}
const MyLittleInput = () => {
return <input onChange={(e) => handleChange(e)} />;
};
return (
<>
{colorList.map((color) => (
<MyLittleInput key={color} />
))}
</>
);
}
I tried different solutions as defining Keys or using useRef but nothing worked
It's too much code to be debugged easily, but for what I can see on the fiddle, there are serveral things wrong, first of all you are doing really too much things for a simple increment/decrement of a input value. But most important you are defining theyr value using the parametresListe state, but never really changing it wit the setParametresListe function, which should be the only way to safely change controlled form inputs.
Just try to do a bit of cleaning on your code and to use the useState as it is meat to be used
Let us know any updates!
UPDATE:
Having a look at your cleaned code, the problem is that a input inside a component gets builded again and again.
The reason for that, is that each input should have they unique "key" prop, so react can easily understand what input is changed and update only that one.
You have 2 ways to make this work, for the first, I've edited your code:
import "./styles.css";
import React, { useState } from "react";
const DEFAULT_INPUT_STATE = {
orange: "",
blue: "",
yellow: ""
};
export default function App() {
let colorList = ["orange", "blue", "yellow"];
const [inputState, setInputState] = useState(DEFAULT_INPUT_STATE);
const handleChange = (e) => {
const { name, value } = e.target;
console.log(name);
setInputState({
...inputState,
[name]: value
});
};
return (
<>
{colorList.map((color, i) => (
<input
key={color}
name={color}
value={inputState[color]}
onChange={(e) => handleChange(e)}
/>
))}
</>
);
}
As you can see, I've just removed the component for the input and did a bit of other changes, but If you still want to use a component, you can moove all the .map function inside of it, but there's no way to create the input inside a component if it is in a .map function
There is too much code, difficult to follow through, in your example. In the nutshell, I see in dev tools, when I update an input, the entire example component is re-rendered, thus all input elements got destroyed and replaced by newly created ones, without focus. It must be just a bug in your code: once an input is updated it renders different stuff, instead of just changing the input value. But it is beyond something someone here would debug for you for free :D

How do I pass data when the x button is clicked?

So I have a filter chip, and this filter chip is just passed a text body, and close function like so:
import CloseIcon from '#mui/icons-material/Close';
import "./FilterChip.css";
function FilterChip({textBody, onCloseClick}) {
return <div className="filter-chip">
Category: {textBody} <CloseIcon onClick={onCloseClick} className="filter-chip-close-button"/>
</div>
}
export default FilterChip;
I can render multiple filter chips in one page. How can I tell my parent component that the particular chip's x button has been clicked? Is it possible to pass this data on the onCloseClick function? I need to remove the chip once it's x button has been clicked, and I also need to uncheck it from my list of check boxes in my parent component. This is how I render the chips.
function renderFilterChips() {
const checkedBoxes = getCheckedBoxes();
return checkedBoxes.map((checkedBox) =>
<FilterChip key={checkedBox} textBody={checkedBox} onCloseClick={onChipCloseClick} />
);
}
You should pass an "identifier" for each chip and then use that identifier to find out "what" was clicked by the user. And then you can filter out the clicked chip.
function FilterChip({ textBody, onCloseClick, id }) {
const handleOnClose = (event) => {
onCloseClick(event, id);
};
return (
<div className="filter-chip">
Category: {textBody}{" "}
<CloseIcon onClick={handleOnClose} className="filter-chip-close-button" />
</div>
);
}
Now your onCloseClick should accept a new param id and handle the logic to remove the chip .
Hope it helps.
Sounds like you need checkedBoxes to be in state.
import { useState } from "react"
const initialBoxes = getCheckedBoxes()
function renderFilteredChips() {
const [ checkedBoxes, setCheckedBoxes ] = useState(initialBoxes)
}
Then implement a function to remove a checked box by its index (or if you have a unique key identifier that would be even better)
const onChipCloseClick = (indexToRemove) => {
setCheckedBoxes(state => state.filter((_, chipIndex) => chipIndex !== indexToRemove))
}
Then when you map over the chips, make sure the function that closes the chip has its index, effectively allowing each chip in state to filter itself out of state, which will re-render your chips for you.
import { useState } from "react"
const initialBoxes = getCheckedBoxes()
function renderFilteredChips() {
const [ checkedBoxes, setCheckedBoxes ] = useState(initialBoxes)
const onChipCloseClick = (indexToRemove) => {
setCheckedBoxes(state => state.filter((_, chipIndex) => chipIndex !== indexToRemove))
}
return <>
{checkedBoxes.map((checkedBox, index) => (
<FilterChip
key={index}
textBody={checkedBox}
onCloseClick={() => onChipCloseClose(index)}
/>
})
</>
}
Obligatory note that I haven't checked this and wrote it in Markdown, so look out for syntax errors (:

useState one behind the rendered element

import React, { useState } from "react";
import { Form } from "react-bootstrap";
import geneList from '../data/SampleSheet.json';
import C3 from 'c3'
import C3Chart from 'c3';
import 'c3/c3.css';
const GENEDATA = [["Gnai3",501, 747, 705, 543, 689], ["genen",3,3,3,3],["Cdc45",22, 30 ,10 ,28, 29]] as [string, ...number[]][];
const GENENAMES = ["Gnai3", "genen", "Cdc45"];
export function Genes(): JSX.Element {
// This is the State (Model)
const [gene, setGene] = useState<([string, ...number[]])>(GENEDATA[0]);
const [geneName, setGeneName] = useState<string>(GENENAMES[0]);
// This is the Control
//no idea why the gene name is one behind but maybe you can figure it out
function updateGene(event: React.ChangeEvent<HTMLSelectElement>) {
setGeneName(event.target.value);
const tempGene = gene;
const changeData = GENEDATA.find((name: [string, ...number[]]): boolean => name[0] === geneName);
if(changeData === undefined){
setGene(tempGene);
alert("Gene not found!");
}
else{
alert(geneName);
setGene(changeData);
}
}
// eslint-disable-next-line #typescript-eslint/no-unused-vars
var chart = C3.generate({
bindto: '#chart',
data: {
columns: [
gene
]
},
axis: {
y: {
label:"Activation Level"
}
},
color: {
pattern: ['#ff7f0e']
},
});
// This is the View
return (
<div>
<Form.Group controlId="Genes">
<Form.Label>Pick gene to display: </Form.Label>
<Form.Select value={geneName} onChange={updateGene}>
{ GENENAMES.map((geneName: string) =>
<option key={geneName} value={geneName}>{geneName}</option>
)}
</Form.Select>
<div id="chart"></div>
</Form.Group>
Selected Gene: {geneName}
</div>
);
}
Im trying to graph some data using the C3 library and the data used in the graph should be synced up with the current geneName, however, it seems that when my page re-renders, the geneName seems to be one ahead of the gene state and I am not sure why.
I thought that maybe it was because state update was being done in on function all together so that the useState wasnt set to the correct value yet which is true. I need my graph data to reflect the current geneName and its data.
I actually figured it out right after creating this post.
It was because i was setting my data name and its respective data equal to the current geneName state. Because the state doesnt change until after the render, it was getting the actual current state which is not what I wanted.
The fix was simply setting the gene to event.target.value insted of geneName
The fix was in my .find

Component not updating data when display turned on using React

I have a component that is not updating with the latest information when it's displayed again. I feel like this has to do with the asynchronous nature of hooks, and I'm still not 100% on async functions or how to update them properly.
const [notes, setNotes] = useState([]);
const [showCreate, setCreate] = useState(false);
const [note, setNote] = useState({});
function toggleCreateCard() {
setCreate(() => !showCreate);
}
function addNote(newNote) {
setNotes(prevNotes => {
return [...prevNotes, newNote];
});
}
The state in question is "note". I have it set to toggle the create card on and off using a boolean which will first get called when created using this below and then when saved will run addNote to add it into an array
function createNewCard(newItem) {
setNote({
header: newItem,
content: "",
checklist: [],
pictures: [],
dueDate: "",
comments: []
});
toggleCreateCard();
}
This function is where I start running into problems. The function below takes an id for the "notes" array and sets note to match the object contained within it, which should be a filled out version of the original item in createNewCard
function showCard(id) {
setNote(notes[id]);
toggleCreateCard();
}
This "ShowCard" gets triggered when the card is clicked on and is supposed to load the card back up with the data from the note array. However, it seems that note is not updating in time and all it pulls is the header that was last set in createNewCard. I've tested and it will show the last assigned title only, and if I say create a second card and click between the two, it'll show the "correct" title but no description or anything else, and the "note" constant is set to the last updated item (for example if I run this once right after creating a note, it's all blank except the title which was originally created when I opened create card (what was set as newItem). If I run this again on the same card and check what note is set to, it will then be set to the note with the id I queried before, even if it's not the same as the one I just ran it with)
Below is the component call in the return statement, which gets displayed when the note is first created and when the note gets clicked on.
{showCreate ? (
<CreateCard
note={note}
addNote={addNote}
createOff={toggleCreateCard}
/>
) : null}
and just for extra details, here's my CreateCard component
import React, { useState } from "react";
import Fab from "#material-ui/core/Fab";
import AddIcon from "#material-ui/icons/AddCircle";
import Checklist from "./CreateCard/Checklist";
import Description from "./CreateCard/Description";
import DueDate from "./CreateCard/DueDate";
import Pictures from "./CreateCard/Pictures";
import Title from "./CreateCard/Title";
function CreateCard(props) {
const [loadNote, setNote] = useState({
header: props.note.header,
content: props.note.content,
checklist: props.note.checklist,
pictures: props.note.pictures,
dueDate: props.note.dueDate,
comments: []
});
function handleChange(event) {
setNote(prevNote => {
return {
...prevNote,
[event.name]: event.value
};
});
}
function submitNote() {
props.addNote(loadNote);
props.createOff();
}
return (
<form
className="create-card form-styling shadow-15px lato"
onKeyPress={e => {
e.key === "Enter" && e.preventDefault();
}}
>
<Title header={loadNote.header} update={handleChange} />
<Description description={loadNote.content} update={handleChange} />
<Checklist checklist={loadNote.checklist} update={handleChange} />
<Pictures pictures={loadNote.pictures} update={handleChange} />
<DueDate duedate={loadNote.dueDate} update={handleChange} />
<Fab color="primary" aria-label="add" size="small" onClick={submitNote}>
<AddIcon />
</Fab>
</form>
);
}
How do I get it to correctly load the object into note and then display the information stored in the object whenever the component is rendered?

Odd behaviour from custom react hook

https://jsfiddle.net/dqx7kb91/1/
In this fiddle, I extracted a part of a project (I tried to simplify it as much as I could without breaking anything), used to manage search filters.
We have many different types of filters defined as an object like this:
filter = {
name: "",
initVal: 1, //default value for the filter,
controlChip: ... // filter's chip component
...
}
the chip component are used to list all filters activated and to edit already activated filters (in this case remove the filter from the list of activated filters).
The filters are contained in an object handled by an unique container containing a custom hook.
The problem is, let's say I set the 1st filter, then set the second and I decide to finally remove the first filter, both filters are removed. Everything is working fine apart from this.
What might be causing this ? It seems to me that in the custom hook useMap that I use, when I try to remove the filter, it doesn't take account of the actual current state but uses the state it was when I added the first filter (an empty object), so it tries to remove something from nothing and set the state to the result, an empty object.
How can I fix this ? Thank you
What's happening is when you set your first filter (let's say #1) you're setting map that contains just filter 1. When you set filter #2 map contains filters #1 & #2. BUT... and here's the thing... your remove callback for filter #1 has map only containing #1, not #2. That's because your callback was set before you set filter #2. This would normally be solved because you're using hooks like useCallback, but the way you're implementing them (createContainer(useFilters)), you're creating separate hooks for each filter object.
I would simplify this into only one component and once it is working start extracting pieces one by one if you really need to.
I know this is a complete rewrite from what you had, but this works:
import React from "react";
import ReactDOM from "react-dom";
const App = () => {
const [map, setMap] = React.useState({});
// const get = React.useCallback(key => map[key], [map])
const set = (key, entry) => {
setMap({ ...map, [key]: entry });
};
const remove = key => {
const {[key]: omit, ...rest} = map;
setMap(rest);
};
// const reset = () => setMap({});
const filters = [
{ name: 'filter1', value: 1 },
{ name: 'filter2', value: 2 },
];
return (
<>
{Object.keys(map).map(key => (
<button onClick={() => remove(key)}>
REMOVE {key}
</button>
))}
<hr />
{filters.map(({ name, value }) => (
<button onClick={() => set(name, { value })}>
SET {name}
</button>
))}
</>
)
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Resources