How can you create a Tree display using dynamic data from an API in React? - javascript

So, I've got a bit of a doozy here. I've looked into trees and the like in react, and I'm fairly confident I can implement one, given the right data structure. The problem I'm running into is that I'm getting my data from an API that doesn't, at least natively, have the structure for a tree, so I'm trying to create that structure on the fly.
This is what the data I'm getting back from the API looks like:
const category = //could have children
{
"object_name":"B2B",
"data_provider_key":"bluekai",
"object_key": "bluekai-31",
"object_type":"category",
};
const segment = //will only be a child
{
"object_name":"B2B > Role/Title > Admin Exec",
"data_provider_key":"bluekai",
"object_key": "bluekai-1145",
"object_type":"segment",
"cpm_cost":2.500
};
And this is the logic that I'm using to try and manipulate the data from the API to add children/create parents, etc.
const asyncView = async function (segTree: string | undefined) {
const categoryDataCall = api.getBeeswaxSegmentView(categoryBody);
const segmentDataCall = api.getBeeswaxSegmentView(segmentBody);
const data = await Promise.all([categoryDataCall, segmentDataCall]);
const parent = categoryData.find( (el: any) => el.object_key === segTree);
const categories = data[0].payload;
if (categories.length >= 1) {
for (let i = 0; i < categories.length; i++) {
categories[i].children = [];
}
}
parent.children = categories.concat(data[1].payload);
setCategoryData(parent.children);
setParent(parent);
}
asyncView(e.currentTarget.dataset.segment_tree);
}
return (
<>
<div>PARENT: {parent.object_name}</div>
{categoryData.length === 0
? <div>No category data</div>
: categoryData.map((e: any) => {
if (e.object_type === 'segment') {
return (
<div data-segment_tree={`${e.object_key || "NULL"}`}
data-provider_key={`${e.data_provider_key}`}
>
{`Name: ${e.object_name} (${e.object_key}, $${parseFloat(e.cpm_cost).toFixed(2)} CPM)`}
</div>
)
}
return (
<div data-segment_tree={`${e.object_key || "NULL"}`}
data-provider_key={`${e.data_provider_key}`}
onClick={getCategoryAndSegmentData}
>
{`Name: ${e.data_provider_name || e.object_name}`}
</div>
)
})
}
</>
);
}
I haven't implemented the Tree part yet, but that's because I am fairly confident I'm not creating the relations between elements correctly in my logic/the logic breaks if there are multiple 'trees'/categories on a page (which there will be.)
Sorry if this is a bit much, but any help or just ideas on dynamically modifying the data from the API to fit the tree structure of child/parent relationships would be appreciated!
Edit in response to Ray Hatfield:
What's the relationship between a category and a segment?
Segments will always be children of Categories, and will never have children of their own. Categories can have other categories as children.
How do you establish which category a segment belongs to?
The object_key property from the Category object gets passed to the API call(s) (two calls are made: one for segments, and one for categories). This is the only relation between segments and categories - nothing else in the return data ties them together.
What is e?
I assume you mean in the e.currentTarget.dataset.segment_tree line.
e is the event object, which I'm using to create the queries and firing them off on click events. I'm storing the object_key in a data-attribute in the HTML, and then passing it to a handler to generate the categoryBody and segmentBody used in the asyncView() function.
For some reason I have to explicitly pass the e.currentTarget.dataset.segment_tree as an argument to the async function even though they're in the same scope, but all it's doing is allowing me to find the Category that was clicked in the existing array of data in state.
What is categoryData?
categoryData is the array of values ( that is currently in state. So, each time I hit the API I update category data to re-render everything.
Effectively, I'm finding the parent (category that was clicked) firing off the API calls to get all the subcategories/segments associated with the clicked categories object_key, and then adding a children prop to any incoming categories, and then setting the children of the last clicked element equal to the returned segments + categories, and then rendering.

I put together this working demo on jsfiddle. Here are the highlights:
The Core Idea
The core idea is a Category component that's responsible for loading and rendering its own segments and subcategories. The subcategories get rendered using the same Category component, resulting in a recursive tree structure.
The Category Component
const Category = ({item}) => {
const [data, setData] = React.useState();
const onClick = data
? () => setData(null) // discard data (collapse) on subsequent click
: () => load(item.object_key).then(setData);
return (
<div className="category">
<div
className={`category-name ${data ? 'open' : ''}`}
onClick={onClick}
>
{item.object_name}
</div>
{data && (
<ul>
{ data.map((child, i) => (
<li key={i}><Node item={child}/></li>
))}
</ul>
)}
</div>
)
}
This component takes a single item prop representing the category. The component expects item to have object_key and object_name fields, like the category object in your example.
Initially the component has no information other than what's in the item, so it renders the category's name with an onClick handler that makes API calls to fetch the category's children and then stores the result in the component's state:
const [data, setData] = React.useState();
const onClick = () => load(item.object_key).then(setData);
On the subsequent render the Category component renders its children (segments and subcategories) in addition to the category name. Subcategories are rendered using the same Category component, resulting in a recursive tree structure.
The Segment Component
const Segment = ({item: {object_name}}) => (
<div className="segment">{object_name}</div>
);
Simple component for rendering segments. Just returns the segment name here, but you could of course expand it to do whatever you need it to do.
The Node Component
const Node = ({item}) => {
const Cmp = item.object_type === 'category' ? Category : Segment;
return <Cmp item={item} />;
};
Convenience component for rendering a <Segment /> or <Category /> for the given item according to its type.
The rest of the example code is just hand waving to simulate the API calls and generate mock data.
load function
const load = async (parentKey) => {
const [categories, segments] = await Promise.all([
mockApiRequest('category'),
mockApiRequest('segment')
]);
return [
...categories,
...segments
];
}
Given a category's object_key, this makes the api calls to get the segments and subcategories, merges and returns the results as a single array.
mockApiRequest
const mockApiRequest = (type) => (
new Promise((resolve) => {
setTimeout(() => resolve(fakeData(type)), 200);
})
)
Simulates the API request. Waits 200ms before resolving with mock data.
fakeData
// generate mock response data
const fakeData = (type) => {
// copy the list of names
const n = [...names];
// plucks a random name from the list
const getName = () => (
n.splice(Math.floor(Math.random() * n.length), 1)[0]
);
// generate and return an array of data
return Array.from(
{length: Math.floor(Math.random() * 5) + 1},
(_, i) => ({
...samples[type],
object_name: getName()
})
)
};
Generates mock category or segment data by copying the sample and choosing a random name.

Related

how to write Foreach with out key in react js [duplicate]

I am making a React app that allows you to make a list and save it, but React has been giving me a warning that my elements don't have a unique key prop (elements List/ListForm). How should I create a unique key prop for user created elements? Below is my React code
var TitleForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var listName = {'name':this.refs.listName.value};
this.props.handleCreate(listName);
this.refs.listName.value = "";
},
render: function() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input className='form-control list-input' type='text' ref='listName' placeholder="List Name"/>
<br/>
<button className="btn btn-primary" type="submit">Create</button>
</form>
</div>
);
}
});
var ListForm = React.createClass({
getInitialState: function() {
return {items:[{'name':'item1'}],itemCount:1};
},
handleSubmit: function(e) {
e.preventDefault();
var list = {'name': this.props.name, 'data':[]};
var items = this.state.items;
for (var i = 1; i < items.length; i++) {
list.data.push(this.refs[items[i].name]);
}
this.props.update(list);
$('#'+this.props.name).remove();
},
handleClick: function() {
this.setState({
items: this.state.items.concat({'name':'item'+this.state.itemCount+1}),
itemCount: this.state.itemCount+1
});
},
handleDelete: function() {
this.setState({
itemCount: this.state.itemCount-1
});
},
render: function() {
var listItems = this.state.items.map(function(item) {
return (
<div>
<input type="text" className="list-form" placeholder="List Item" ref={item.name}/>
<br/>
</div>
);
});
return (
<div>
<form onSubmit={this.handleSubmit} className="well list-form-container">
{listItems}
<br/>
<div onClick={this.handleClick} className="btn btn-primary list-button">Add</div>
<div onClick={this.handleDelete} className="btn btn-primary list-button">Delete</div>
<button type="submit" className="btn btn-primary list-button">Save</button>
</form>
</div>
)
}
});
var List = React.createClass({
getInitialState: function() {
return {lists:[], savedLists: []};
},
handleCreate: function(listName) {
this.setState({
lists: this.state.lists.concat(listName)
});
},
updateSaved: function(list) {
this.setState({
savedLists: this.state.savedLists.concat(list)
});
},
render: function() {
var lst = this;
var lists = this.state.lists.map(function(list) {
return(
<div>
<div key={list.name} id={list.name}>
<h2 key={"header"+list.name}>{list.name}</h2>
<ListForm update={lst.updateSaved} name={list.name}/>
</div>
</div>
)
});
var savedLists = this.state.savedLists.map(function(list) {
var list_data = list.data;
list_data.map(function(data) {
return (
<li>{data}</li>
)
});
return(
<div>
<h2>{list.name}</h2>
<ul>
{list_data}
</ul>
</div>
)
});
var save_msg;
if(savedLists.length == 0){
save_msg = 'No Saved Lists';
}else{
save_msg = 'Saved Lists';
}
return (
<div>
<TitleForm handleCreate={this.handleCreate} />
{lists}
<h2>{save_msg}</h2>
{savedLists}
</div>
)
}
});
ReactDOM.render(<List/>,document.getElementById('app'));
My HTML:
<div class="container">
<h1>Title</h1>
<div id="app" class="center"></div>
</div>
There are many ways in which you can create unique keys, the simplest method is to use the index when iterating arrays.
Example
var lists = this.state.lists.map(function(list, index) {
return(
<div key={index}>
<div key={list.name} id={list.name}>
<h2 key={"header"+list.name}>{list.name}</h2>
<ListForm update={lst.updateSaved} name={list.name}/>
</div>
</div>
)
});
Wherever you're lopping over data, here this.state.lists.map, you can pass second parameter function(list, index) to the callback as well and that will be its index value and it will be unique for all the items in the array.
And then you can use it like
<div key={index}>
You can do the same here as well
var savedLists = this.state.savedLists.map(function(list, index) {
var list_data = list.data;
list_data.map(function(data, index) {
return (
<li key={index}>{data}</li>
)
});
return(
<div key={index}>
<h2>{list.name}</h2>
<ul>
{list_data}
</ul>
</div>
)
});
Edit
However, As pointed by the user Martin Dawson in the comment below, This is not always ideal.
So whats the solution then?
Many
You can create a function to generate unique keys/ids/numbers/strings and use that
You can make use of existing npm packages like uuid, uniqid, etc
You can also generate random number like new Date().getTime(); and prefix it with something from the item you're iterating to guarantee its uniqueness
Lastly, I recommend using the unique ID you get from the database, If you get it.
Example:
const generateKey = (pre) => {
return `${ pre }_${ new Date().getTime() }`;
}
const savedLists = this.state.savedLists.map( list => {
const list_data = list.data.map( data => <li key={ generateKey(data) }>{ data }</li> );
return(
<div key={ generateKey(list.name) }>
<h2>{ list.name }</h2>
<ul>
{ list_data }
</ul>
</div>
)
});
It is important to remember that React expects STABLE keys, meaning you should assign the keys once and every item on your list should receive the same key every time, that way React can optimize around your data changes when it is reconciling the virtual DOM and decides which components need to re-render.
So, if you are using UUID you need to do it at the data level, not at the UI level.
Also keep in mind you can use any string you want for the key, so you can often combine several fields into one unique ID, something like ${username}_${timestamp} can be a fine unique key for a line in a chat, for example.
Keys helps React identify which items have changed/added/removed and should be given to the elements inside the array to give the elements a stable identity.
With that in mind, there are basically three different strategies as described bellow:
Static Elements (when you don't need to keep html state (focus, cursor position, etc)
Editable and sortable elements
Editable but not sortable elements
As React Documentation explains, we need to give stable identity to the elements and because of that, carefully choose the strategy that best suits your needs:
STATIC ELEMENTS
As we can see also in React Documentation, is not recommended the use of index for keys "if the order of items may change. This can negatively impact performance and may cause issues with component state".
In case of static elements like tables, lists, etc, I recommend using a tool called shortid.
1) Install the package using NPM/YARN:
npm install shortid --save
2) Import in the class file you want to use it:
import shortid from 'shortid';
2) The command to generate a new id is shortid.generate().
3) Example:
renderDropdownItems = (): React.ReactNode => {
const { data, isDisabled } = this.props;
const { selectedValue } = this.state;
const dropdownItems: Array<React.ReactNode> = [];
if (data) {
data.forEach(item => {
dropdownItems.push(
<option value={item.value} key={shortid.generate()}>
{item.text}
</option>
);
});
}
return (
<select
value={selectedValue}
onChange={this.onSelectedItemChanged}
disabled={isDisabled}
>
{dropdownItems}
</select>
);
};
IMPORTANT: As React Virtual DOM relies on the key, with shortid every time the element is re-rendered a new key will be created and the element will loose it's html state like focus or cursor position. Consider this when deciding how the key will be generated as the strategy above can be useful only when you are building elements that won't have their values changed like lists or read only fields.
EDITABLE (sortable) FIELDS
If the element is sortable and you have a unique ID of the item, combine it with some extra string (in case you need to have the same information twice in a page). This is the most recommended scenario.
Example:
renderDropdownItems = (): React.ReactNode => {
const elementKey:string = 'ddownitem_';
const { data, isDisabled } = this.props;
const { selectedValue } = this.state;
const dropdownItems: Array<React.ReactNode> = [];
if (data) {
data.forEach(item => {
dropdownItems.push(
<option value={item.value} key={${elementKey}${item.id}}>
{item.text}
</option>
);
});
}
return (
<select
value={selectedValue}
onChange={this.onSelectedItemChanged}
disabled={isDisabled}
>
{dropdownItems}
</select>
);
};
EDITABLE (non sortable) FIELDS (e.g. INPUT ELEMENTS)
As a last resort, for editable (but non sortable) fields like input, you can use some the index with some starting text as element key cannot be duplicated.
Example:
renderDropdownItems = (): React.ReactNode => {
const elementKey:string = 'ddownitem_';
const { data, isDisabled } = this.props;
const { selectedValue } = this.state;
const dropdownItems: Array<React.ReactNode> = [];
if (data) {
data.forEach((item:any index:number) => {
dropdownItems.push(
<option value={item.value} key={${elementKey}${index}}>
{item.text}
</option>
);
});
}
return (
<select
value={selectedValue}
onChange={this.onSelectedItemChanged}
disabled={isDisabled}
>
{dropdownItems}
</select>
);
};
Hope this helps.
Do not use this return `${ pre }_${ new Date().getTime()}`;. It's better to have the array index instead of that because, even though it's not ideal, that way you will at least get some consistency among the list components, with the new Date function you will get constant inconsistency. That means every new iteration of the function will lead to a new truly unique key.
The unique key doesn't mean that it needs to be globally unique, it means that it needs to be unique in the context of the component, so it doesn't run useless re-renders all the time. You won't feel the problem associated with new Date initially, but you will feel it, for example, if you need to get back to the already rendered list and React starts getting all confused because it doesn't know which component changed and which didn't, resulting in memory leaks, because, you guessed it, according to your Date key, every component changed.
Now to my answer. Let's say you are rendering a list of YouTube videos. Use the video id (arqTu9Ay4Ig) as a unique ID. That way, if that ID doesn't change, the component will stay the same, but if it does, React will recognize that it's a new Video and change it accordingly.
It doesn't have to be that strict, the little more relaxed variant is to use the title, like Erez Hochman already pointed out, or a combination of the attributes of the component (title plus category), so you can tell React to check if they have changed or not.
edited some unimportant stuff
Let React Assign Keys To Children
You may leverage React.Children API:
const { Children } = React;
const DATA = [
'foo',
'bar',
'baz',
];
const MyComponent = () => (
<div>
{Children.toArray(DATA.map(data => <p>{data}</p>))}
</div>
);
ReactDOM.render(<MyComponent />,document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
To add the latest solution for 2021...
I found that the project nanoid provides unique string ids that can be used as key while also being fast and very small.
After installing using npm install nanoid, use as follows:
import { nanoid } from 'nanoid';
// Have the id associated with the data.
const todos = [{id: nanoid(), text: 'first todo'}];
// Then later, it can be rendered using a stable id as the key.
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
)
Another option is weak-key: https://www.npmjs.com/package/weak-key
import weakKey from "weak-key";
const obj1 = {a : 42};
const obj2 = {b : 123};
const obj3 = {a : 42};
console.log(weakKey(obj1)); // 'weak-key-1'
console.log(weakKey(obj2)); // 'weak-key-2'
console.log(weakKey(obj3)); // 'weak-key-3'
console.log(weakKey(obj1)); // 'weak-key-1'
For a simple array of text-strings; I'm trying one of the two ways:
1. encodeURI which is available on both; NodeJS and browser
const WithEncoder = () => {
const getKey = useCallback((str, idx) => encodeURI(`${str},${idx}`), [])
return (
<div>
{["foo", "bar"].map((str, idx) => (
<div key={getKey(str, idx)}>{str}</div>
))}
</div>
)
}
2. window.btoa which is available only in browser.
const WithB2A = () => {
const getKey = useCallback((str, idx) => window.btoa(`${str}-${idx}`), [])
return (
<div>
{["foo", "bar"].map((str, idx) => (
<div key={getKey(str, idx)}>{str}</div>
))}
</div>
)
}
Depends on the situation, choose a uniqueId creator is ok when you just want render silly items, but if you render items like drag&drop etc and you haven't any uniqueId for each item, I recommend remap that data in your redux, mapper, wherever and add for each item an uniqueId (and not in the render like <Item key={...}) because React couldn't perform any check between renders (and with that all the benefits).
With that remapped that you can use that new Id in your Component.
Here is what I have done, it works for reordering, adding, editing and deleting. Once set the key is not changed, so no unnecessary re-render. One PROBLEM which may be a show stopper for some: it requires adding a property to your object at first render say "_reactKey".
Example for functional component in psuedo TS (ie it won't run in snippets):
interface IRow{
myData: string,
_reactKey?:number
}
export default function List(props: {
rows: Array<IRow>
}) {
const {myRows} = props;
const [nextKey, setNextKey] = useState(100);
const [rows, setRows] = useState<Array<IRow>|undefined>();
useEffect(function () {
if (myRows) {
for (let row of myRows){
if (!row._reactKey){
row._reactKey = nextKey;
setNextKey(nextKey+1);
}
}
setRows(myRows);
} else if (!rows) {
setRows([]);
}
}, [myRows, columns]);
addRow(){
let newRow = { blah, blah, _reactKey : nextKey};
setNextKey(nextKey+1);
rows.push(newRow);
setRows({...rows});
}
function MyRow(props:{row:IRow}){
const {row} = props;
return <tr><td>{row._reactKey}</td><td>row.myData</td></tr>
}
return <table>
<tr><th>Index</th><th>React Key</th><th>My Data</th></tr>
rows.map((row, key)=>{
return <MyRow key={row._reactKey} row={row} />
}
</table>
}
I don't use react too much, but the last time I saw this issue I just created a new state array, and tracked the keys there.
const [keys, setKeys] = useState([0]);
const [items, setItems] = useState([value: "", key: 0,])
Then when I add a new item to list, I get the last key from the keys array, add 1, then use setKeys to update the keys array. Something like this:
const addItemWithKey = () => {
// create a new array from the state variable
let newKeyArr = [...keys];
// create a new array from the state variable that needs to be tracked with keys
let newItemArr = [...items];
// get the last key value and add 1
let key = newKeyArr[newKeyArr.length-1] + 1;
newKeyArr.push(key);
newItemArr.push({value: "", key: key,});
// set the state variable
setKeys(newKeyArr);
setItems(newItemArr);
};
I don't worry about removing values from the keys array because it's only being used for iterating in the component, and we're trying to solve for the case where we remove an item from the list and/or add a new item. By getting the last number from the keys array and adding one, we should always have unique keys.
import React, {useState} from 'react';
import {SafeAreaView,ScrollView,StyleSheet,Text,View,Dimensions} from 'react-native';
const {width}=Dimensions.get('window');
function sayfalar(){
let pages=[]
for (let i = 0; i < 100; i++) {
pages.push(<View key={i} style={styles.pages}><Text>{i}</Text></View>)
}
return pages
}
const App=()=>{
return(
<View style={styles.container}>
<ScrollView horizontal={true} pagingEnabled={true}>
{sayfalar()}
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
container:{
flexDirection:'row',
flex:1
},
pages:{
width:width
}
})
export default App;
Use the mapped index (i)
things.map((x,i) => {
<div key=i></div>
});
Hope this helps.
You can use react-html-id to generate uniq id easely : https://www.npmjs.com/package/react-html-id
The fastest solution in 2021 is to use uniqid: Go to https://www.npmjs.com/package/uniqid for more info but to sum up:
First in your terminal and your project file: npm install uniqid
Import uniqid in your project
Use it in any key that you need!
uniqid = require('uniqid');
return(
<div>
<div key={ uniqid() } id={list.name}>
<h2 key={ uniqid() }>{list.name}</h2>
<ListForm update={lst.updateSaved} name={list.name}/>
</div>
</div>
)
});
I am using this:
<div key={+new Date() + Math.random()}>

Why am I getting an empty array and incorrect data fetching in console?

I have two buttons, the movies and tvshows button. When I click on either I change the option to the opposite one as shown on the handleMovieClick and handleTVShowsClick methods. Movies and TVshows may have the same id, that is why the option is important for fetching.
I am printing to the console.
(1) array of movie/tv ids
(2) the option selected
(3) each individual object fetched based on the id and option
When I use the value to calculate the array of movie ids available the first time, I first get an empty array and then the expected result. That is the array of movie ids, option is movie and each individual promise objects is fulfilled.
Here's the kicker, when I click on the tvshows button, I am getting the following two things consoled.
(1) same movie array of ids not updated, option changes to tv, however I get a bunch of promises with "status_message: "The resource you requested could not be found" because I am basically trying to retrieve promises with movie ids, but tvshow option.
Then,
(2) Then the result is changed and I everything as expected. That is option stays the same, changed in above step, the array gets updated, and the individual promise objects are all fulfilled.
What is happening?
I get that it is happening because of the code in my JSX where I am making use of the array ids. I think I need to only call the array.map() part after the useEffect has run and the so array is updated but how to do this and why is useEffect not running the first time?
Here is the code.
const Results = () => {
const options = ['movie', 'tv'];
const { value } = useParams(); // from router
const [ page, setPage ] = useState(1);
const [ option, setOption ] = useState(options[0]);
const [ array, setArray ] = useState([]);
const handleMovieClick = () => {setOption('movie')}
const handleTVShowClick = () => {setOption('tv')}
useEffect(() => {
console.log('HERE');
fetchArrayByPage(value, page, option).then(res => setArray(res));
}, [value, page, option])
console.log(array);
console.log(option);
return (
<div className="results-container">
<div>
<ul className='categories'>
<button onClick={handleMovieClick}>Movies<span>{0}</span></button>
<button onClick={handleTVShowClick}>TV Shows<span>{0}</span></button>
</ul>
</div>
{
<div>
<div className='results-list'>
{array.map((arr, index) => {
console.log(fetchID(arr, option));
return <Card key={index} id={arr} option={option}></Card>})}
</div>
<div className='pages'>
</div>
</div>
}
</div>
);
}
useEffect's callback is fired after the render and fetchArrayByPage seems to be async. So clicking on "TV shows" results in:
option is changed to "tv" (array stays the same)
console.log(array); console.log(option);
render with new option and old array
console.log('HERE'); fetchArrayByPage(...)
some time passes
setArray(res)
render with new option and new array
You should call fetchArrayByPage from your handle*Click handlers:
const handleTVShowClick = () => {
fetchArrayByPage(value, page, "tv").then(res => {
setOption('tv');
setArray(res);
});
}

How to filter an array without losing data?

I have a function to filter an array of todos. The array is filtered correctly but when I type some thing else it removes other array elements and doesn't go back.
I've seen a few solutions, but they say "you have to write code in the render function" and I don't use this method because I render the array elsewhere with the help of props. filteredTodos has all local storage data.
How can I filter this data without losing data?
// *** Form.js ***
const filterTodos = (e) => {
const filtTod = filteredTodos.filter((todo) => {
return todo.text.toLowerCase().indexOf(e.target.value.toLowerCase()) !== -1
});
setFilteredTodos([...filtTod]);
};
// it's my render place *** TodoList.js ***
return (
<div className="todo-container">
<ul className="todo-list">
{filteredTodos.map((todo) => {
return (
<Todo
id={todo.id}
key={todo.id}
text={todo.text}
completed={todo.completed}
setTodos={setTodos}
todos={todos}
/>
)
})}
</ul>
</div>
);
const [inputText, setInputText] = useState("");
const [todos, setTodos] = useState([]); // Todos is all array items.
const [status, setStatus] = useState("all");
const [filteredTodos, setFilteredTodos] = useState([]); // the state is for my filter list
The idea with filtering data like an array of todos is to maintain a consistent "reference" or "source" array that represents all of the todos that exist in your app. This reference array may change when the user adds or removes a todo item, and may be persisted to the backend (or local storage, in your case) in some capacity or other. This array is the source of truth.
When a user enters a search/filtering term, filter on the reference/source array of all todos every time rather than the already-filtered result set.
In your code, todos represents the source array and filteredTodos represents the results of a single search. By using filteredTodos.filter, the results either keep diminishing after each filtering query, or there never were any results to work with to begin with -- either way, it's incorrect.
Think of filteredTodos as a temporary snapshot of todos used to show a subset view of todos to match the user's search term.
The fix is to use todos.filter rather than filteredTodos.filter so that you're always searching on the full data set.

SOLVED: Redux selector - calculating the difference between two selector returned values

SOLUTION at the bottom
I have a dropdown menu and as I choose an element from it - it should no longer be available in the dropdown.
I fetch from the api available products, store them in redux - same with chosen products.
What I want to do is have an array of all the products(done), array of the chosen products(done), and array that is being displayed on the front end is the list of all the products, filtering out elements that are in both arrays.
If I do calculation in the component, it only works after remounting the component.
const productsAvailable = state => state.products.allProducts;
const productsChosen = state =>state.newproducts.allProducts;
the calculation I do is:
const productsToChoseFrom =
productsAvailable &&
productsAvailable.filter(
el =>
productsChosen &&
!productsChosen.map(product => product).includes(el)
);
How would I do this calculation in a selector?
//the answer is - using a creteSelector from reselect
const productsToChoseFrom = createSelector(
productsAvailable,
productsChosen,
(productsOff, productsRendere) =>
productsOff &&
productsOff.filter(el =>
productsRendere
? !productsRendere.map(cycle => cycle.interval).includes(el)
: productsOff
)
);
what I was actually missing is the ternary, that's why it wouldn't update..

How can I give a key in JSX the value of a variable depending on conditions

I'm learning React by implementing a front-end interface for the note app API that I created. I have succeeded in having a list of all the note titles in my database appear. I want to be able to click on a title and have the note expand into the text of the note. The easiest way I've found for this is to give the "key" attribute of the 'li' as a variable and to also declare the same variable in the JSX { } object because they have the same name.
I've been looking for an answer for this for a few days and have been unable to find this exact problem. You can put a variable in a normal JSX expression but I need to do it on the 'li' which means technically in the HTML.
Here's some code to understand what I'm saying.
const NoteData = () => {
const [titles, setTitles] = useState([]);
const [open, setOpen] = useState(false);
useEffect(() => {
//AXIOS CALL
setTitles(response.data[0]);
});
}, []);
//^^^^^add the array there to stop the response.data from repeating WAY TOO MANY TIMES
let listTitles = titles.map(titles => (
<li className="noteTitles" key={titles.title}>
{titles.title}
</li>
));
let showText = titles.map(titles => (
<li className="openText" key= {titles.text_entry}>
{titles.text_entry}
</li>
))
let openNote = () => {
setOpen(open => !open);
if (open) {
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{showText}
</ul>
</div>
);
}
if (!open) {
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{listTitles}
</ul>
</div>
);
}
};
return { openNote };
};
export default NoteData;
That is the code I currently have. Here's showing a more simplified version of the openNote function that maybe makes more sense and shows what I'm trying to do:
VariableHere = "";
let openNote = () => {
setOpen(open => !open);
open ? (VariableHere = titles.text_entry) : (VariableHere = titles.title);
};
let listNotes = titles.map(titles => (
<li className="noteTitles" key={VariableHere}>
{VariableHere}
</li>
));
return (
<div>
<ul onClick={openNote}>
{listNotes}
</ul>
</div>
);
On click of each element there should be a switch of the key elements so if the element is 'open' the key variable and given variable in the JSX object should be mapped to titles.text_entry and on '(!open)' the key and JSX should be mapped to titles.title.
first of all, you're using a ternary in a weird way:
open ? (VariableHere = titles.text_entry) : (VariableHere = titles.title);
Ternaries are meant to be expressions whose value is conditional, but you're using it like a shorthand if/else. Try something like
VariableHere = open ? titles.text_entry : titles.title;
which is both shorter and more readable.
Second of all, keys in an array of elements are meant to help React determine which elements to update, if an item represents the same object, its key shouldn't change. In this case, regardless of what you're displaying, an item in the array represents the same note. Always using the title as the key should be fine provided items can't have the same title. If they can, use some sort of unique ID instead. If the order of the items doesn't change throughout the life of the component, using the array index as the key is fine.
Lastly, what you seem to want to do is called "conditional rendering". There are many ways to achieve this in react, one such way is to use the pre-cited ternary operator. Here is a minimal working example:
const listNotes = titles.map(note => (
<li className="noteTitles" key={note.title}>
{open ? note.title : note.text_entry}
</li>
));
const openNote = () => {
setOpen(!open);
}
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{listNotes}
</ul>
</div>
)
You could also use a ternary in the key expression, but as I talked about above, it's not a good idea to do so.
Given your data-structure, I think you can simplify your code a bit. There is no need to create separate arrays for titles and contents. It sounds like you just want to expand and collapse a note when it is selected.
Here is a really simplified version on how you an do this. I'll use a sample data-set since we don't have access to your API.
const NoteData = () => {
const [titles, setTitles] = useState([]);
const [currentNote, setCurrentNote] = useState({});
useEffect(() => {
//AXIOS CALL
// setTitles(response.data[0]);
let data = [
{ id: 1, title: "a", text_entry: "what" },
{ id: 2, title: "b", text_entry: "is" },
{ id: 3, title: "c", text_entry: "up?" }
];
setTitles(data);
}, []);
const handleClick = noteId => {
let selectedTitle = titles.find(title => title.id == noteId);
//"collapse" if already selected
if (noteId === currentNote.id) {
setCurrentNote({});
} else {
setCurrentNote(selectedTitle);
}
};
let listTitles = titles.map(title => (
<li
className="noteTitles"
key={title.title}
onClick={() => handleClick(title.id)}
>
{title.title}
{title.id === currentNote.id && <div>{title.text_entry}</div>}
</li>
));
return (
<div>
Click on link item
<ul>{listTitles}</ul>
</div>
);
};
See working sandbox: https://codesandbox.io/s/old-silence-366ne
The main updates:
You don't need to have an "open" state. To be more succinct and
accurate, you should have a currentNote state instead, which is
set when clicking on a list item.
Have your handleClick function accept a noteId as an argument.
Then use that noteId to find the corresponding note in your titles
state. Set that found note as the currentNote. If the selected
note was already the currentNote, simply set currentNote to an
empty object {}, thus creating our expanding/collapsing effect.
In the JSX, after the title, use a ternary operator to conditionally
display the currentNote. If the note being mapped matches the
currentNote, then you would display a div containing the
text_entry.

Categories