React functional component to accept rendering function - reactjs

I would like to find out how to write a functional component that would accept an array of objects as prop (dataSource), then render all the array objects based on a RenderFunction which I would pass as another prop. I want the render function to accept a parameter that would represent an object from the array.
Check this code snippet and the comments:
// This is my data
var dummyData = [{
"id": 1,
"name": "item1"
},
{
"id": 2,
"name": "item2"
},
{
"id": 3,
"name": "item3"
},
{
"id": 4,
"name": "item4"
},
{
"id": 5,
"name": "item5"
},
{
"id": 6,
"name": "item6"
}
]
// Functional Component :
function ListComponent(props) {
return (
<div className={"ListContainer"}>
{props.dataSource.map((x) => {
return (
<div key={x.id} className="ListItemContainer">
{props.itemRender}{" "}
</div>
);
})}{" "}
</div>
);
}
// This is my render function which I'm passing as a prop.
// I would like it to accept an argument which would become
// an object of dummy data array.
// For example if I passed x to it, the x inside the function on the first
// .map iteration would become {"id": 1,"name": "item1"}
function itemRenderTest() {
return (
<p> This is item </p>
// I want to be able to render it like this
// <p> This is {x.name} </p>
)
}
// passing props and rendering
ReactDOM.render(
<ListComponent
dataSource={dummyData}
itemRender={itemRenderTest()}
/>,
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>
The snippet illustrates some of the desired functionality. I can write a render function, but don't know how can I change the code so the render function could accept a parameter which would represent an object from the array.
I want to be able to write the render function like this:
function itemRenderTest(x){
return(
<p>This is {x.name}</p>
)
}
The Component would receive 2 props. The first - dataSrouce would specify the JSON array. The second - render function would define how the child list components are being rendered
<ListComponent
dataSource={dummyData}
itemRender={itemRenderTest}
/>
I'm trying to recreate a reusable component similar to what a lot of DevExtreme react components do. They basically just accept a render function definition like this renderItem={renderItemFunction} and it just works. I want to write my List component so it does the same This is a good example of one of their components
Is this possible with React? Code snippets would be really helpful.

That's 100% possible in React and a super common pattern. If I understand your question correctly -- what I typically do in this situation is
Define a parent component for the list items. It will handle fetching or otherwise retrieving the array of objects data, the overall state of that data, and the render logic for the individual list components
ListItem component, which is stateless (pure component) and simply renders reusable components based on data passed in as props. That's how component libraries create reusable components, like the one you mentioned
const ItemsList = () => {
// state variable which will store the list data
const [listData, setListData] = useState([])
// let's assume the list items are fetched from an API on initial render
useEffect(() => {
// fetch logic for list items, then update state variable
// with resulting data
const listItems = axios("https://yourapi.com/listitems")
.then(({ data }) => setListData(data)
.catch(err => console.info("Fetch error", err))
)
}, [])
const renderList = useMemo(() => listData.map(
(listItemData) => <ListItem data={listItemData}/>),
[listData])
return (
<div>
{renderList}
</div>
)
}
const ListItem = (props) => {
const { data } = props;
return (
// populate the component with the values from the data object
)
}
A few things to point out here:
useEffect hook with [] dependency will only run once on first render, retrieve the necessary data, and update the state variable initialized with the useState hook. This can be done in other ways too
useMemo hook allows us to define the render logic for the individual list components, and memoize the evaluated result. That way, this function won't run on every render, unless the value of the listData variable changes. The function provided to the useMemo hook iterates through the array of objects, and renders a ListItem components with the respective data
The ListItem component then simply receives the data as a prop and renders it
Edit based on the updated answer:
I haven't tested it but this approach should work.
const ItemsList = (props) => {
const { data, itemRender: ItemRender } = props;
const renderItems = () => data.map((itemData) => <ItemRender data={itemData}/>)
return (
<div>
{renderItems()}
</div>
)
}
const ListItem = (props) => {
const { data } = props;
return (
// populate the component with the values from the data object
)
}
const App = () => {
const data = retrieveData()
return (
<ItemsList data={data} itemRender={ListItem}/>
)
}
App component retrieves the data, decides on the component that it will use to render the individual item (ListItem) and passes both of those as props to ItemsList
ItemsList then simply maps the data to each individual component
Edit 2: basic working snippet
const ItemsList = (props) => {
const { data, itemRender: ItemRender } = props;
const renderItems = () => data.map((itemData, i) => <ItemRender data={itemData.val} key={i}/>)
return (
<div>
{renderItems()}
</div>
)
}
const ListItem = (props) => {
const { data } = props;
console.info(data)
return (
<div
style={{
width: 100,
height: 100,
border: "2px solid green",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}>
{data}
</div>
)
}
const App = () => {
const data = [{val: 1}, {val: 2}, {val: 3}]
return (
<div
style={{
width: "100vw",
height: "100vh",
backgroundColor: "white",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}>
<ItemsList data={data} itemRender={ListItem}/>
</div>
)
}
export default App;

Related

Get the ref of an element rendering by an array

I'm coding a tab navigation system with a sliding animation, the tabs are all visible, but only the selected tab is scrolled to. Problem is that, I need to get the ref of the current selected page, so I can set the overall height of the slide, because that page may be taller or shorter than other tabs.
import React, { MutableRefObject } from 'react';
import Props from './Props';
import styles from './Tabs.module.scss';
export default function Tabs(props: Props) {
const [currTab, setCurrTab] = React.useState(0);
const [tabsWidth, setTabsWidth] = React.useState(0);
const [currentTabHeight, setCurrentTabHeight] = React.useState(0);
const [currentTabElement, setCurrentTabElement] = React.useState<Element | null>(null);
const thisRef = React.useRef<HTMLDivElement>(null);
let currentTabRef = React.useRef<HTMLDivElement>(null);
let refList: MutableRefObject<HTMLDivElement>[] = [];
const calculateSizeData = () => {
if (thisRef.current && tabsWidth !== thisRef.current.offsetWidth) {
setTabsWidth(() => thisRef.current.clientWidth);
}
if (currentTabRef.current && currentTabHeight !== currentTabRef.current.offsetHeight) {
setCurrentTabHeight(() => currentTabRef.current.offsetHeight);
}
}
React.useEffect(() => {
calculateSizeData();
const resizeListener = new ResizeObserver(() => {
calculateSizeData();
});
resizeListener.observe(thisRef.current);
return () => {
resizeListener.disconnect();
}
}, []);
refList.length = 0;
return (
<div ref={thisRef} className={styles._}>
<div className={styles.tabs}>
{ props.tabs.map((tab, index) => {
return (
<button onClick={() => {
setCurrTab(index);
calculateSizeData();
}} className={currTab === index ? styles.tabsButtonActive : ''} key={`nav-${index}`}>
{ tab.label }
<svg>
<rect rx={2} width={'100%'} height={3} />
</svg>
</button>
)
}) }
</div>
<div style={{
height: currentTabHeight + 'px',
}} className={styles.content}>
<div style={{
right: `-${currTab * tabsWidth}px`,
}} className={styles.contentStream}>
{ [ ...props.tabs ].reverse().map((tab, index) => {
const ref = React.useRef<HTMLDivElement>(null);
refList.push(ref);
return (
<div ref={ref} style={{
width: tabsWidth + 'px',
}} key={`body-${index}`}>
{ tab.body }
</div>
);
}) }
</div>
</div>
</div>
);
}
This seems like a reasonable tab implementation for a beginner. It appears you're passing in content for the tabs via a prop named tabs and then keeping track of the active tab via useState() which is fair.
Without looking at the browser console, I believe that React doesn't like the way you are creating the array of refs. Reference semantics are pretty challenging, even for seasoned developers, so you shouldn't beat yourself up over this.
I found a good article that discusses how to keep track of refs to an array of elements, which I suggest you read.
Furthermore, I'll explain the differences between that article and your code. Your issues begin when you write let refList: MutableRefObject<HTMLDivElement>[] = []; According to the React hooks reference, ref objects created by React.useRef() are simply plain JavaScript objects that are persisted for the lifetime of the component. So what happens when we have an array of refs like you do here? Well actually, the contents of the array are irrelevant--it could be an array of strings for all we care. Because refList is not a ref object, it gets regenerated for every render.
What you want to do is write let refList = React.useRef([]), per the article, and then populate refList.current with refs to your child tabs as the article describes. Referring back to the React hooks reference, the object created by useRef() is a plain JavaScript object, and you can assign anything to current--not just DOM elements.
In summary, you want to create a ref of an array of refs, not an array of refs. Repeat that last sentence until it makes sense.

React state not updating when used outside hook

I'm playing around with a hook that can store some deleted values. No matter what I've tried, I can't get the state from this hook to update when I use it in a component.
const useDeleteRecords = () => {
const [deletedRecords, setDeletedRecords] = React.useState<
Record[]
>([]);
const [deletedRecordIds, setDeletedRecordIds] = React.useState<string[]>([]);
// ^ this second state is largely useless – I could just use `.filter()`
// but I was experimenting to see if I could get either to work.
React.useEffect(() => {
console.log('records changed', deletedRecords);
// this works correctly, the deletedRecords array has a new item
// in it each time the button is clicked
setDeletedRecordIds(deletedRecords.map((record) => record.id));
}, [deletedRecords]);
const deleteRecord = (record: Record) => {
console.log(`should delete record ${record.id}`);
// This works correctly - firing every time the button is clicked
setDeletedRecords(prev => [...prev, record]);
};
const wasDeleted = (record: Record) => {
// This never works – deletedRecordIds is always [] when I call this outside the hook
return deletedRecordIds.some((r) => r === record.id);
};
return {
deletedRecordIds,
deleteRecord,
wasDeleted,
} // as const <-- no change
}
Using it in a component:
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecordIds, wasDeleted, deleteRecord } = useDeleteRecords();
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
}
React.useEffect(() => {
console.log('should fire when deletedRecordIds changes', deletedRecordIds);
// Only fires once for each row on load? deletedRecordIds never changes
// I can rip out the Ids state and do it just with deletedRecords, and the same thing happens
}, [deletedRecordIds]);
}
If it helps, these are in the same file – I'm not sure if there's some magic to exporting a hook in a dedicated module? I also tried as const in the return of the hook but no change.
Here's an MCVE of what's going on: https://codesandbox.io/s/tender-glade-px631y?file=/src/App.tsx
Here's also the simpler version of the problem where I only have one state variable. The deletedRecords state never mutates when I use the hook in the parent component: https://codesandbox.io/s/magical-newton-wnhxrw?file=/src/App.tsx
problem
In your App (code sandbox) you call useDeleteRecords, then for each record you create a DisplayRecord component. So far so good.
function App() {
const { wasDeleted } = useDeleteRecords(); // ✅
console.log("wtf");
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord record={record} /> // ✅
</div>
) : null;
})}
</div>
);
}
Then for each DisplayRecord you call useDeleteRecords. This maintains a separate state array for each component ⚠️
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecords, deleteRecord } = useDeleteRecords(); // ⚠️
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
};
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
// Only fires once for each row on load? deletedRecords never changes
}, [deletedRecords]);
return (
<div>
<div>{record.id}</div>
<div onClick={handleDelete} style={{ cursor: "pointer" }}>
[Del]
</div>
</div>
);
};
solution
The solution is to maintain a single source of truth, keeping handleDelete and deletedRecords in the shared common ancestor, App. These can be passed down as props to the dependent components.
function App() {
const { deletedRecords, deleteRecord, wasDeleted } = useDeleteRecords(); // 👍🏽
const handleDelete = (record) => (event) { // 👍🏽 delete handler
deleteRecord(record);
};
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord
record={record}
deletedRecords={deletedRecords} // 👍🏽 pass prop
handleDelete={handleDelete} // 👍🏽 pass prop
/>
</div>
) : null;
})}
</div>
);
}
Now DisplayRecord can read state from its parent. It does not have local state and does not need to call useDeleteRecords on its own.
const DisplayRecord = ({ record, deletedRecords, handleDelete }) => {
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
}, [deletedRecords]); // ✅ passed from parent
return (
<div>
<div>{record.id}</div>
<div
onClick={handleDelete(record)} // ✅ passed from parent
style={{ cursor: "pointer" }}
children="[Del]"
/>
</div>
);
};
code demo
I would suggest a name like useList or useSet instead of useDeleteRecord. It's more generic, offers the same functionality, but is reusable in more places.
Here's a minimal, verifiable example. I named the delete function del because delete is a reserved word. Run the code below and click the ❌ to delete some items.
function App({ items = [] }) {
const [deleted, del, wasDeleted] = useSet([])
React.useEffect(_ => {
console.log("an item was deleted", deleted)
}, [deleted])
return <div>
{items.map((item, key) =>
<div className="item" key={key} data-deleted={wasDeleted(item)}>
{item} <button onClick={_ => del(item)} children="❌" />
</div>
)}
</div>
}
function useSet(iterable = []) {
const [state, setState] = React.useState(new Set(...iterable))
return [
Array.from(state), // members
newItem => setState(s => (new Set(s)).add(newItem)), // addMember
item => state.has(item) // isMember
]
}
ReactDOM.render(
<App items={["apple", "orange", "pear", "banana"]}/>,
document.querySelector("#app")
)
div.item { display: inline-block; border: 1px solid dodgerblue; padding: 0.25rem; margin: 0.25rem; }
[data-deleted="true"] { opacity: 0.3; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Since you are updating deletedRecordIds inside a React.useEffect, this variable will have the correct value only after the render complete. wasDeleted is a closure that capture the value of deletedRecordIds when the component renders, thus it always have a stale value. As yourself are suggesting, the correct way to do that is to use .filter() and remove the second state.
Talking about the example you provided in both cases you are defining 5 hooks: one hook for each DisplayRecord component and one for the App. Each hook define is own states, thus there are 5 deletedRecords arrays on the page. Clicking on Del, only the array inside that specific component will be updated. All other component won't be notified by the update, because the state change is internal to that specific row. The hook state in App will never change because no one is calling its own deleteRecord function.
You could solve that problem in 2 way:
Pulling up the state: The hook is called just once in the App component and the deleteRecord method is passed as parameter to every DisplayRecord component. I updated your CodeSandbox example.
Use a context: Context allows many component to share the same state.

How should I update individual items' className onClick in a list in a React functional component?

I'm new to React and I'm stuck trying to get this onClick function to work properly.
I have a component "Row" that contains a dynamic list of divs that it gets from a function and returns them:
export function Row({parentState, setParentState}) {
let divList = getDivList(parentState, setParentState);
return (
<div>
{divList}
</div>
)
}
Say parentState could just be:
[["Name", "info"],
["Name2", "info2"]]
The function returns a list of divs, each with their own className determined based on data in the parentState. Each one needs to be able to update its own info in parentState with an onClick function, which must in turn update the className so that the appearance of the div can change. My code so far seems to update the parentState properly (React Devtools shows the changes, at least when I navigate away from the component and then navigate back, for some reason), but won't update the className until a later event. Right now it looks like this:
export function getDivList(parentState, setParentState) {
//parentState is an array of two-element arrays
const divList = parentState.map((ele, i) => {
let divClass = "class" + ele[1];
return (
<div
key={ele, i}
className={divClass}
onClick={() => {
let newParentState =
JSON.parse(JSON.stringify(parentState);
newParentState[i][1] = "newInfo";
setParentState(newParentState);}}>
{ele[0]}
</div>
)
}
return divList;
}
I have tried to use useEffect, probably wrong, but no luck. How should I do this?
Since your Row component has parentState as a prop, I assume it is a direct child of this parent component that contains parentState. You are trying to access getDivList in Row component without passing it as a prop, it won't work if you write your code this way.
You could use the children prop provided by React that allow you to write a component with an opening and closing tag: <Component>...</Component>. Everything inside will be in the children. For your code it would looks like this :
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const App = () => {
const [parentState, setParentState] = React.useState([
['I am a div', 'bg-red'],
['I am another div', 'bg-red'],
]);
React.useEffect(
() => console.log('render on ParentState changes'),
[parentState]
);
const getDivList = () => {
return parentState.map((ele, i) => {
return (
<div
key={(ele, i)}
className={ele[1]}
onClick={() => {
// Copy of your state with the spread operator (...)
let newParentState = [...parentState];
// We don't know the new value here, I just invented it for the example
newParentState[i][1] = [newParentState[i][1], 'bg-blue'];
setParentState(newParentState);
}}
>
{ele[0]}
</div>
);
});
};
return <Row>{getDivList()}</Row>;
};
const Row = ({ children }) => {
return <>{children}</>;
};
render(<App />, document.getElementById('root'));
And a bit of css for the example :
.bg-red {
background-color: darkred;
color: white;
}
.bg-blue {
background-color:aliceblue;
}
Here is a repro on StackBlitz so you can play with it.
I assumed the shape of the parentState, yu will have to adapt by your needs but it should be something like that.
Now, if your data needs to be shared across multiple components, I highly recommand using a context. Here is my answer to another post where you'll find a simple example on how to implement a context Api.

What is the best way to update an object in a usestate and see the change immediately

const TestScreen = (props) => {
const [data, setData] = useState([
{ "id": "0", "user": "Lisa","amount": 1 },
]);
return(
<View style={{
flex:1,
alignItems:'center',
justifyContent:'center'
}}>
<Text>amount : {data[0].amount}</Text>
<Text>User : {data[0].user}</Text>
<Button title="update" onPress={()=>setData(??????)}></Button>
</View>
)
}
export default TestScreen;
what is the best way to add an amount number on the user Lisa? i can do
// setData([{ "id": "0", "user": "Lisa","amount": data[0].amount + 1}])
but what i have 5 users or 20?
even with a returning function nothing gets updated exept console logged witch show me the actual value
let countAddFunc =(getArr)=>{
let arr = getArr
arr[0].amount++
console.log(arr[0].amount);
return(
arr
)
}
<Button title="update" onPress={()=>setData(countAddFunc(data))}></Button>
One of the key concepts in React is Do Not Modify State Directly. This can be tricky sometimes, especially when dealing with nested data like in your example (a number as a property of an object inside an array).
Below, I refactored your code and added comments to help explain. By creating a function to update the state, and passing that function to each child component's props, the child component will be able to update the state by calling the function.
const {useState} = React;
// component dedicated to displaying each item in the array
const Item = (props) => (
<div style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<div>Amount: {props.item.amount}</div>
<div>User: {props.item.user}</div>
<button onClick={() => {
props.updateAmount(props.item.user);
}}>Increment</button>
</div>
);
const ItemList = () => {
const [data, setData] = useState([
{
id: '0',
user: 'Lisa',
amount: 1,
},
{
id: '1',
user: 'Walter',
amount: 3,
},
]);
const updateAmount = (user, changeAmount = 1) => {
// find the index of the item we want
const index = data.findIndex(item => item.user === user);
// return early (do nothing) if it doesn't exist
if (index === -1) return;
const item = data[index];
// don't modify state directly (item is still the same object in the state array)
const updatedItem = {
...item,
amount: item.amount + changeAmount,
};
// again, don't modify state directly: create new array
const updatedArray = [...data];
// insert updated item at the appropriate index
updatedArray[index] = updatedItem;
setData(updatedArray);
};
return (
<ul>
{data.map(item => (
<li key={item.id}>
<Item item={item} updateAmount={updateAmount} />
</li>
))}
</ul>
);
};
ReactDOM.render(<ItemList />, document.getElementById('root'));
<script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script>
<div id="root"></div>
can you try like this once, pass second param as user id you want to update
<Button title="update" onPress={()=>setData(countAddFunc(data, 0))}></Button>
let countAddFunc =(getArr, id)=>{
const arrCopy = [...getArr]
const user = arrCopy.find(u => u.id === id )
if (user) {
user.amount++
}
return arrCopy
}
actually you are modifying the state directly, and we can not update state directly and getArr is just a refrence to the data in state, so, we created a copy of array, and modified copied array, and then we set this new array into the state, about the code throwing undefined error, add a check, if (user) user.amount++ and make sure id you send onPress={()=>setData(countAddFunc(data, 0))} actually exist
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately. setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall.
Read the official documentation for more info here
Also, alternatively you can use useRef or useEffect with dependency array if you want the callback to immediately fire and do the changes.

React Hooks : mapping data from api call to render pie chart

need some help here. Thank you!
I am making a call to an api which returns an object with two numbers and a list, like so ...
{2, 1 , []}
The codebase I am working with is using 'use-global-hook' for Redux state management.
So I have storeState, storeActions as global hooks.
So in my render I have verified that the number values exist.
return
(
<h3> storeState.value1</h3> //prints 2
<h3> storeState.value2 </h3> //prints 1
)
Tried mapping this way and this throws too many renders error.
How can I map the dataset to Pie chart ?
const test1 = (props) =>
{
const [piChartData, setpiChart] = useState({});
React.useEffect(()=>{
storeActions.callApi();
},[]);
function drawPieChart()
{
setpiChart({
labels: ['label1', 'label2'],
datasets:[{
data:[storeState.value1, storeState.value2],
backgroundColor: ['red','green']
}]
})
}
return(
{drawPieChart()}
<Pie data={piChartData}/>
)
}
drawPieChart calls setpiChart updating the state and causing the component to re-render, on each render, you call drawPieChart, which calls setpiChart , causing an infinite loop of rendering,
remove drawPieChart() from render and put it in useEffect so it gets called when the component mounts, if callApi is aync and returns a promise, use .then :
const test1 = (props) => {
const [piChartData, setpiChart] = useState({});
React.useEffect(() => {
storeActions.callApi();
drawPieChart();
/**
* OR
* storeActions.callApi().then(() => drawPieChart(););
*/
}, []);
function drawPieChart() {
setpiChart({
labels: ["label1", "label2"],
datasets: [
{
data: [storeState.value1, storeState.value2],
backgroundColor: ["red", "green"]
}
]
});
}
return <Pie data={piChartData} />;
};

Resources