How to pass props to subcomponent in Reactjs - javascript

I am attempting to pass props from a parent component function to a child in React without any luck. I am utilizing the React Data Table Component's filtering functionality. The example documentation in Storybook is great, however I want to change the search box into a list of tags, that when clicked, will filter the data table just the same.
The data I'm trying to parse into individual "tags" is structured like this:
[{"id":"09090","first_name":"Cynthia","last_name":"McDonald","email":"email1#gmail.com","profile_url":"https:myprofile.com/1","types":["professor","science"]},
{"id":"03030","first_name":"Ryan","last_name":"Burke","email":"email2#gmail.com","profile_url":"https://myprofile.com/2","types":["student","science"]},
{"id":"05050","first_name":"Stewart","last_name":"Hook","email":"email3#gmail.com","profile_url":"https://myprofile.com/3","types":["professor","math"]}]
I am trying to create a unique tag list of the "types" attribute that acts as a filter onClick instead of onChange, just like the original search textbox example. So based on the sample data, we would end up with tags for professor, science, student, and math.
If you look at the code in Storybook, more specifically, line 45:
const subHeaderComponentMemo = React.useMemo(() =>
<Filter onFilter={value => setFilterText(value)} />, []);
My data is loaded via an API call and I am trying to pass that data to the subHeaderComponentMemo with props, i.e.,
const subHeaderComponentMemo = React.useMemo(() =>
<Filter data={people} onFilter={value => setFilterText(value)} />, []);
I am then trying to receive and loop through that data on line 20 and am replacing most of that code so that it will render the unique tags from the types attribute of the data.
Storybook code:
const Filter = ({ onFilter }) => (
<TextField id="search" type="search" role="search" placeholder="Search Title" onChange={e => onFilter(e.target.value)} />
);
My failed code
const Filter = ({props, onFilter }) => {
// Get a unique array of types
const types = [...new Set(props.types.map(type => type))];
return types.map(type => (
<span
className="badge badge-primary"
onClick={() => onFilter({ type })}
onKeyDown={() => onFilter({ type })}
tabIndex="0"
role="button"
>
{type}
</span>
));
};
This is of course resulting in an epic fail. The module isn't even displaying.
I'm new to React, so any insight/help would be greatly appreciated.
Updated code based on feedback
const Filter = ({ data, onFilter }) => {
console.dir(data);
if (data.length === 0) {
return <div>No data</div>;
}
const types = [...new Set(data.types.map(type => type))];
return (
<>
{types.map(type => (
<span
className="badge badge-primary"
onClick={() => onFilter({ type })}
onKeyDown={() => onFilter({ type })}
tabIndex="0"
role="button"
>
{type}
</span>
))}
;
</>
);
};
One of the errors I got when I made the changes was "cannot read property of 'map' undefined", which I equated to the fact that this component may be rendering before the data gets there and it is empty. So I added an if with a return in case this happens.
I also rewrote the return the statement in Filter based on the feedback because it did look like it would loop and try to render a new component for each iteration.
However, the issue still stands with the data not being present. The table is now loading with the data, but this component is just returning the "No data" div as if nothing ever happened. I'm not sure if this is due to the useMemo hook or what. Besides that, no errors.
Thank you for your time and feedback.
Another update
UseMemo needed the prop to be a dependency in its array... I'm finally seeing data when I console.log it.
const subHeaderComponentMemo = useMemo(
() => <Filter data={people} onFilter={value => setFilterText(value)} />,
[people]
);
However, I'm getting the 'map' undefined error again, as if my data is not in the structure I expect it to be in. I have not changed it and this is the code I'm using to try to access it (as shared before):
const types = [...new Set(data.types.map(type => type))];
Thanks again everyone... getting closer!

you should rewrite you component signature to match the one you are trying to use
const Filter = ({props, onFilter }) => // you are expecting a props name props
<Filter data={people} onFilter={value => setFilterText(value)} />, []); // you are passing data and setFilterText
so you should change it to
const Filter = ({data, onFilter }) =>

Related

React key error although all elements have unique identifier

So I am getting used to React and I understand when using the map function each item needs to be assigned a unique key. I believe I am doing this correctly
return (
countries.map(country => {
return <>
<div key={country.name.common}>
{country.name.common} <button onClick={() => setCountries([country])}>show</button>
</div>
</>
})
)
I still receive an error in the console saying:
Warning: Each child in a list should have a unique "key" prop.
at CountryList.
at div.
Where CountryList being the file the code is extracted from. I have tried adding the same key to the button element and also tried giving the button element its own unique key.
descriptive key error message
countries is a call to "https://restcountries.com/v3.1/all" api.
useEffect(() => {
axios.get("https://restcountries.com/v3.1/all")
.then(response => setCountries(response.data))
}, [])
const handleFilterChange = (event) => {
setFilter(event.target.value)
const filtered = countries.filter(country => country.name.common.toLowerCase().includes(event.target.value))
setCountries(filtered)
}
console error message along with what the app looks like
The key must be placed on the parent element that you are returning from the map function.
Since it's a fragment in this case, you can't directly assign a key to it, unless you use the actual fragment component.
// Can't assign a key
<></>
// Can assign a key
<React.Fragment key={...}></React.Fragment>
Then again, if you only have the div here, why do you need the fragment?
A shorter syntax of your code would look like this:
return (
countries.map(country => (
<div key={country.name.common}>
{country.name.common}
<button onClick={() => setCountries([country])}>show</button>
</div>
))
)
You are using <> as a perent tag and add key in child <div> tag. remove <></> as you dont need this or use Fragment insted
return countries.map(country => (
<div key={country.name.common}>
{country.name.common}
<button onClick={() => setCountries([country])}>show</button>
</div>
))
or
return countries.map(country => {
return (
<React.Fragment key={country.name.common}>
<div>
{country.name.common} <button onClick={() => setCountries([country])}>show</button>
</div>
</React.Fragment>
)
})

Accessing array indices in React

I am new to React and Javascript as a whole. I am simply trying to access array indices after a fetch. I can log a specific index after a file update, but when I refresh the page, I get an 'Undefined' error. I feel as if I am doing something wrong with my state, but I don't know enough about React to debug this. if I attempt to access " users[0].name", I get the "TypeError: Cannot read properties of undefined (reading 'name')
(anonymous function).
import React, { useEffect, useState } from "react";
import { Container, Card, Divider, Typography } from "#mui/material";
const RestData = () => {
const [data, setData] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users/")
.then((response) => response.json())
.then((data) => setData(data));
}, []);
return (
<Container>
{data.map((users) => {
return (
<Card
data={data}
key={users.id}
sx={{ width: "300px", mt: 2, background: "#ff9800" }}
>
<Typography variant="h6" color="seagreen">
{users.name}
</Typography>
<Divider />
{users.email}
<Divider />
{users.company.name}
<Divider />
{users.phone}
<Divider />
{users.website}
<Divider />
</Card>
);
})}
</Container>
);
};
export default RestData;
Although I don't see this occurring in the code you posted, you are getting that particular error because you are trying to access .name when the object
being accessed is undefined.
I presume you are trying to do something like data[0].name.
The issue is, your data array starts empty, so if you try to access any index of an empty array, it will be undefined, you will get that error.
The solution is to make sure the data array already has it's elements in before you access an index of it.
Now, there are many ways to do this. Because it doesn't seem to be a part of your code, it is hard to tell you the best way. But here is a couple:
1- Do an if check before attempting to access the property:
if (data[0]) {
console.log(data[0].name)
// do stuff
}
2- Check if the property exists first using the && operator:
For this case, the attempt to access the name is only done if data[0] is truthy.
console.log(data[0] && data[0].name)
3- Use optional chaining. This is the better one imo, since it allows you to skip the if and will only attempt to access a property if it is not undefined.
console.log(data[0]?.name)
So now to put this in practice, here is a sample app:
const App = () => {
const [data, setData] = React.useState([]);
React.useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users/")
.then((response) => response.json())
.then((data) => setData(data));
}, []);
console.log("data", data);
return (
<div>
<div>
<h3>Example by accessing the index directly with Optional Chaining:</h3>
<p>{data[0] && data[0].name}</p>
</div>
<div>
<h3>Example with the map</h3>
{(data).map((d) => {
return <p>{d.name}</p>;
})}
</div>
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Since you're fetching the data at rendering time you don't have users[0].name, so what I would suggest is to use the conditional operator ?.
So you have something like this
data[0]?.name
What happens in this case is that typescript try to access the property name at the given index, if the property is undefined returns null otherwise returns the value.
I've also made a quick sandbox for demonstration.

React table library that supports mutable data ( useRef )

Basically i'm looking for react table library that can take a mutable object ( to be specific an useRef object) as the main source of data to be displayed.
Basically i want to do something like this:
const TableViewComponent = () =>{
const tableData = useRef([{ rowName: 'test', value:0}] -> basically an array of objects that represents data for every row (the structure doesnt matter)
# code that receives data from server and updates the tableData.current with the data needed
return(
<Table data={tableData.current}/>
)
}
Basically, since i get a bunch of messages from the server and i update the data constantly (the number of rows stays the same), i don't want to rerender the table everytime. So i want to use the useRef to change the data thats being displayed on the table without triggering a rerender from react.
Im not sure if its something that can be done but any help is appreciated :). I tried react-table, rc-table but they didnt seem to work.
Basically, it looks to me like you'll have to do it yourself.
There's some libraries that might help you (like useTable which focuses on headless components) but I don't think they offer what you're looking for.
So let's quickly do a mock-up! (note: this is a quick sketch, assume that the undefined variables are stored somewhere else or are given from the fetch)
function useTableData({ initialData, itemsPerPage, ...etc }) {
const data = useRef(initialData);
const [loading, setLoading] = useState(false);
useEffect(() => {
data.current = fetchFromSomeWhere(...etc);
() => (data.current = null);
}, [etc /* Place other dependencies that invalidate out data here*/]);
const handleNewPage = useCallback(
async ({ pageIndex }) => {
if (!data.current[pageIndex * itemsPerPage]) {
setLoading(true);
data.current = [...data.current, await fetchMoreData(pageIndex)];
}
setLoading(false);
return data.current;
},
[itemsPerPage, data, setLoading],
);
return [data, handleNewPage, loading];
}
Notice how every single thing returned from this hook is a constant reference except for the loading! Meaning, that we can safely pass this to a memoized table, and it won't trigger any re-renders.
const TableContainer = memo(etc => {
const [data, handleNewPage, loading] = useDataForTable(...etc);
return (
<>
{loading && <Spinner />}
{/* Look ma, no expensive-renders! */}
<Body {...{ data }} />
<Pagination {...{ handleNewPage }} />
<OtherActions />
</>
);
});
However, you might have noticed that the body won't re-render when we click on the next page! Which was the whole point of pagination! So now we need some sort of state that'll force the Body to re-render
const TableContainer = memo(etc => {
const [currentPage, setPage] = useState(0);
const [data, handleNewPage, loading] = useDataForTable(...etc);
return (
<>
{loading && <Spinner />}
{/* We're still re-rendering here :( */}
<Body {...{ data, currentPage }} />
<Footer {...{ handleNewPage, setPage }} />
<OtherActions />
</>
);
});
And so we're left with a table that to use properly we need to:
Exercise restraint and properly invalidate old data. Otherwise, we'd be displaying incorrect data.
'Unpack' the data from the current ref on the Body component, and then render it.
In essence, after all that work we're still left with a solution isn't particularly attractive, unless you have some really complicated actions or some expensive scaffolding around the TableComponent itself. This might however be your case.

How to add function to "X" icon in Material UI Searchbar?

I have the following code:
import SearchBar from "material-ui-search-bar";
const data = [
{
name: "Jane"
},
{
name: "Mark"
},
{
name: "Jason"
}
];
export default function App() {
const [results, setResults] = useState();
const filteredResults = data.filter((item) => {
return Object.keys(item)?.some((key) => {
return item[key].includes(results?.toLowerCase());
});
});
return (
<div className="App">
<SearchBar
value={results}
onChange={(value) => setResults(value)}
placeholder="Please enter name..."
/>
{filteredResults.map((item) => {
return <li>{item.name}</li>;
})}
</div>
);
}
codesandbox
when I delete name from search bar using delete keyboard all names from data are displayed below the search bar, but if I click X button, it clears the search bar but doesn't display all names. Is there a way to add a function to this X button so when I click it, it acts the same way as delete keyboard?
You can pass a function to the onCancelSearch prop to reset the results state variable.
<SearchBar
value={results}
onChange={(value) => setResults(value)}
onCancelSearch={() => setResults('')}
/>
Suggestions
It's better to initialize results with an empty string. You can now remove the ? in results?.toLowerCase() since results will never be nullish (undefined or null).
const [results, setResults] = useState('')
You should pass the key prop to the li element. You can add an id property to the items array to use as the key or use the item index.
{
filteredResults.map((item) => (
<li key={item.id}>{item.name}</li>
))
}
And there are a couple of issues with the filtering logic.
You're converting the search query to lowercase but not the name. In your example, if you search for 'ja', nothing would show up even though matches exist (Jane and Jason).
filteredResults will throw an error if any of the object values do not have the includes method (You can reproduce the issue by adding a numeric id field to the array items). You could fix it by using a searchableKeys array to only perform the search in specific fields.
const searchableKeys = ['name']
const filteredResults = data.filter((item) =>
searchableKeys.some((key) =>
item[key].toLowerCase().includes(results.toLowerCase())
)
)
I would recommend renaming results to query or searchQuery for clarity.
Hello upon checking your problem, the reason why its remapping the list on delete key (in keyboard) because it triggers the onChange event of the searchBar to have the same event as the searchBar, i've tried it on my end and it seems that this solution can be solve your issue
<SearchBar
value={results}
onChange={(value) => setResults(value)}
placeholder="Please enter name..."
closeIcon={<button onClick={() => setResults("")}>clear</button>}
/>
the closeIcon props - overrides the close icon and its methods..
here is the documentation that i check material-ui-search-bar
here also the replicated/solved code-sandbox

Handle React re-rendering

I have a similar situation like the one in the sandbox.
https://codesandbox.io/s/react-typescript-fs0em
Basically what I want to achieve is that Table.tsx is my base component and App component is acting like a wrapper component. I am returning the JSX from the wrapper file.
Everything is fine but the problem is whenever I hover over any name, getData() is called and that is too much rerendering. Here it is a simple example but in my case, in real, the records are more.
Basically Table is a generic component which can be used by any other component and the data to be displayed in can vary. For e.g. rn App is returning name and image. Some other component can use the Table.tsx component to display name, email, and address. Think of App component as a wrapper.
How can I avoid this getData() to not to be called again and again on hover?
Can I use useMemo or what approach should I use to avoid this?
Please help
Every time you update the "hover" index state in Table.jsx it rerenders, i.e. the entire table it mapped again. This also is regenerating the table row JSX each time, thus why you see the log "getData called!" so much.
You've effectively created your own list renderer, and getData is your "renderRow" function. IMO Table shouldn't have any state and the component being rendered via getData should handle its own hover state.
Create some "row component", i.e. the thing you want to render per element in the data array, that handles it's own hover state, so when updating state it only rerenders itself.
const RowComponent = ({ index, name }) => {
const [hov, setHov] = useState();
return (
<div
key={name}
onMouseEnter={() => setHov(index)}
onMouseLeave={() => setHov(undefined)}
style={{ display: "flex", justifyContent: "space-around" }}
>
<div> {name} </div>
<div>
<img
src={hov === index ? img2 : img1}
height="30px"
width="30px"
alt=""
/>
</div>
</div>
);
};
Table.jsx should now only take a data prop and a callback function to render a specific element, getData.
interface Props {
data: string[];
getData: () => JSX.Element;
}
export const Table: React.FC<Props> = ({ data, getData }) => {
return (
<div>
{data.map((name: string, index: number) => getData(name, index))}
</div>
);
};
App
function App() {
const data = ["Pete", "Peter", "John", "Micheal", "Moss", "Abi"];
const getData = (name: string, index: number, hov: number) => {
console.log("getData called!", index);
return <RowComponent name={name} index={index} />;
};
return <Table data={data} getData={getData} />;
}

Categories

Resources