Trigger re-render of subcomponent (react-table) using hooks - reactjs

I'm still new to React, and functional programming, and Javascript, and JSX, so go easy if this is a stupid question.
I'm modifying one of the example material-ui tables from react-table v7. The original code can be found here. The example is completely functional and is using React Hooks as opposed to classes, as do all of the components of the template I'm using (shout out to creative-tim.com!)
My parent function (representative of a page in my dashboard application), for instance Users.js or Stations.js fetches data from a backend api inside a useEffect hook. That data is then passed as a prop to my subcomponent ReactTables.js
For some reason ReactTables.js does not receive changes to the "data" prop after the parent page's useEffect finishes. However, once I modify the data from a subcomponent of ReactTables (in this case AddAlarmDialog.js) then the table re-renders and all of my data suddenly appears.
How can I trigger the re-render of my subcomponent when data is returned from the parent component's useEffect? I noticed that in older versions of React there was a lifecycle function called componentWillReceiveProps(). Is this the behavior I need to emulate here?
Example Parent Component (Alarms.js):
import React, { useEffect, useState } from "react";
// #material-ui/core components
// components and whatnot
import GridContainer from "components/Grid/GridContainer.js";
import GridItem from "components/Grid/GridItem.js";
import ReactTables from "../../components/Table/ReactTables";
import { server } from "../../variables/sitevars.js";
export default function Alarms() {
const [columns] = useState([
{
Header: "Alarm Name",
accessor: "aName"
},
{
Header: "Location",
accessor: "aLocation"
},
{
Header: "Time",
accessor: "aTime"
},
{
Header: "Acknowledged",
accessor: "aAcked"
},
{
Header: "Active",
accessor: "aActive"
}
]);
const [data, setData] = useState([]);
const [tableType] = useState("");
const [tableLabel] = useState("Alarms");
useEffect(() => {
async function fetchData() {
const url = `${server}/admin/alarms/data`;
const response = await fetch(url);
var parsedJSON = JSON.parse(await response.json());
var tableElement = [];
parsedJSON.events.forEach(function(alarm) {
tableElement = [];
parsedJSON.tags.forEach(function(tag) {
if (alarm.TagID === tag.IDX) {
tableElement.aName = tag.Name;
}
});
tableElement.aTime = alarm.AlarmRcvdTime;
parsedJSON.sites.forEach(function(site) {
if (site.IDX === alarm.SiteID) {
tableElement.aLocation = site.Name;
}
});
if (alarm.Active) {
tableElement.aActive = true;
} else {
tableElement.aActive = false;
}
if (!alarm.AckedBy && !alarm.AckedTime) {
tableElement.aAcked = false;
} else {
tableElement.aAcked = true;
}
//const newData = data.concat([tableElement]);
//setData(newData);
data.push(tableElement);
});
}
fetchData().then(function() {
setData(data);
});
}, [data]);
return (
<div>
<GridContainer>
<GridItem xs={12} sm={12} md={12} lg={12}>
<ReactTables
data={data}
columns={columns}
tableType={tableType}
tableLabel={tableLabel}
></ReactTables>
</GridItem>
</GridContainer>
</div>
);
}
Universal Table Subcomponent (ReactTables.js):
import React, { useState } from "react";
// #material-ui/core components
import { makeStyles } from "#material-ui/core/styles";
// #material-ui/icons
import Assignment from "#material-ui/icons/Assignment";
// core components
import GridContainer from "components/Grid/GridContainer.js";
import GridItem from "components/Grid/GridItem.js";
import Card from "components/Card/Card.js";
import CardBody from "components/Card/CardBody.js";
import CardIcon from "components/Card/CardIcon.js";
import CardHeader from "components/Card/CardHeader.js";
import { cardTitle } from "assets/jss/material-dashboard-pro-react.js";
import PropTypes from "prop-types";
import EnhancedTable from "./subcomponents/EnhancedTable";
const styles = {
cardIconTitle: {
...cardTitle,
marginTop: "15px",
marginBottom: "0px"
}
};
const useStyles = makeStyles(styles);
export default function ReactTables(props) {
const [data, setData] = useState(props.data);
const [columns] = useState(props.columns);
const [tableType] = useState(props.tableType);
const [skipPageReset, setSkipPageReset] = useState(false)
const updateMyData = (rowIndex, columnId, value) => {
// We also turn on the flag to not reset the page
setData(old =>
old.map((row, index) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value
};
}
return row;
})
);
};
const classes = useStyles();
return (
<GridContainer>
<GridItem xs={12}>
<Card>
<CardHeader color="primary" icon>
<CardIcon color="primary">
<Assignment />
</CardIcon>
<h4 className={classes.cardIconTitle}>{props.tableLabel}</h4>
</CardHeader>
<CardBody>
<EnhancedTable
data={data}
columns={columns}
tableType={tableType}
setData={setData}
updateMyData={updateMyData}
skipPageReset={skipPageReset}
filterable
defaultPageSize={10}
showPaginationTop
useGlobalFilter
showPaginationBottom={false}
className="-striped -highlight"
/>
</CardBody>
</Card>
</GridItem>
</GridContainer>
);
}
ReactTables.propTypes = {
columns: PropTypes.array.isRequired,
data: PropTypes.array.isRequired,
tableType: PropTypes.string.isRequired,
tableLabel: PropTypes.string.isRequired,
updateMyData: PropTypes.func,
setData: PropTypes.func,
skipPageReset: PropTypes.bool
};
**For the record: if you notice superfluous code in the useEffect it's because I was messing around and trying to see if I could trigger a re-render.

I dont know exactly how the reactTable is handling its rendering, but if its a pure functional component, then the props you pass to it need to change before it will re-evaluate them. When checking if props have changed, react will just do a simple === comparison, which means that if your props are objects whos properties are being modified, then it will still evaluate as the same object. To solve this, you need to treat all props as immutable
In your example, you are pushing to the data array, and then calling setData(data) which means that you are passing the same instance of the array. When react compares the previous version of data, to the new version that you are setting in the call to setDate, it will think data hasnt changed because it is the same reference.
To solve this, you can just make a new array from the old array by spreading the existing array into a new one. So, instead of doing
data.push(tableElement);
You should do
const newInstance = [...data, tableElement];
Your code will need some tweaking because it looks like you are adding in lots of tableElements. But the short version of the lesson here is that you should never try and mutate your props. Always make a new instance
EDIT: So, after looking again, I think the problem is the way you are using the default param in the useState hook. It looks like you are expecting that to set the state from any prop changes, but in reality, that param is simply the default value that you will put in the component when it is first created. Changing the incoming data prop doesn't alter your state in any way.
If you want to update state in response to changes in props, you will need to use the useEffect hook, and set the prop in question as a dependancy.
But personally, I would try and not have what is essentially the same data duplicated in state in two places. I think the best bet would be to store your data in your alarm component, and add a dataChanged callback or something which will take your new data prop, and pass it back up to alarm via a parameter in the callback

Related

Unable to prevent Flatlist from re-rendering (already using useCallback and memo)

I'm building a to do list app as part of a coding course, using Firebase Realtime Database and React Native with Expo.
I have no problems rendering the to do list, and in this case clicking a checkbox to indicate whether the task is prioritized or not.
However, each time I click on the checkbox to change the priority of a single task in the to do list, the entire Flatlist re-renders.
Each task object is as follows:
{id: ***, text: ***, priority: ***}
Task Component: (It consists of the text of the to do (task.text), and also a checkbox to indicate whether the task is prioritized or not). I've wrapped this component in React.memo, and the only props passed down from Todolist to Task are the individual task, but it still re-renders every time. (I left out most of the standard imports in the code snippet below)
import { CheckBox } from '#rneui/themed';
const Task = ({
item,
}) => {
console.log(item)
const { user } = useContext(AuthContext);
const onPressPriority = async () => {
await update(ref(database, `users/${user}/tasks/${item.id}`), {
priority: !item.priority,
});
};
return (
<View
style={{ flexDirection: 'row', alignItems: 'center', width: '95%' }}
>
<View
style={{ width: '90%' }}
>
<Text>{item.text}</Text>
</View>
<View
style={{ width: '10%' }}
>
<CheckBox
checked={item.priority}
checkedColor="#00a152"
iconType="material-community"
checkedIcon="checkbox-marked"
uncheckedIcon={'checkbox-blank-outline'}
onPress={onPressPriority}
/>
</View>
</View>
)}
export default memo(Task, (prevProps, nextProps) => {
if (prevProps.item !== nextProps.item) {
return true
}
return false
})
To Do List parent component: Contains the Flatlist which renders a list of the Task components. It also contains a useEffect to update the tasks state based on changes to the Firebase database, and the function (memoizedOnPressPriority) to update the task.priority value when the Checkbox in the task component is clicked. Since memoizedOnPressPriority is passed a prop to , I've tried to place it in a useCallback, but is still re-rendering all items when the checkbox is clicked. (I left out most of the standard imports in the code snippet below)
export default function Home2() {
const { user } = useContext(AuthContext);
const [tasks, setTasks] = useState([]);
useEffect(() => {
if (user) {
return onValue(ref(database, `users/${user}/tasks`), (snapshot) => {
const todos = snapshot.val();
const tasksCopy = [];
for (let id in todos) {
tasksCopy.push({ ...todos[id], id: id });
}
setTasks(tasksCopy);
});
} else {
setTasks([]);
}
}, [user]);
const renderItem = ({ item }) => (
<TaskTwo
item={item}
/>
);
return (
<View style={styles.container}>
<FlatList
data={tasks}
initialNumToRender={5}
windowSize={4}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
</View>
);
}
Could anyone let me know what I'm doing wrong, and how I can prevent the entire Flatlist from re-rendering each time I invoke the memoizedOnPressPriority function passed down to the Task component from the TodoList parent component? Any help is much appreciated!
The flamegraph for the render is below:
Update: I moved the prioritize function (memoizedOnPressPriority) into the Task component and removed the useCallback - so it's not being passed as a prop anymore. The re-render still happens whenever I press it.
Update 2: I added a key extractor , and also a custom equality function into the memoized task component. Still keeps rendering!
I'm not familiar with Firebase Realtime Database, but if I understand the logic correctly, the whole tasks array is updated when one item changes, and this is what is triggering the list update.
Fixing the memo function
Wrapping the Task component in memo does not work because it performs a shallow comparison of the objects. The objects change each time the data is updated because a new tasks array with new objects is created, so the references of the objects are different.
See this post for more details.
To use memo, we have to pass a custom equality check function, that returns true if the component is the same with new props, like so:
export default memo(Task, (prevProps, nextProps) => {
if (prevProps.item.id === nextProps.item.id && prevProps.item.priority === nextProps.item.priority ) {
return true;
}
return false;
})
Note that is the text is modifiable, you'll want to check that too.
Alternative solution : read data from the Task component
This solution is recommended and takes full advantage of Firebase Realtime Database.
To update only the component that is updated, you need to pass an array of ids to your flatlist, and delegate the data reading to the child component.
It's a pattern I use with redux very often when I want to update a component without updating the whole flatlist.
I checked the documentation of Firebase Realtime Database, and they indeed encourage you to read data at the lowest level. If you have a large list with many properties, it's not performant to receive the whole list when only one item is updated. Under the hood, the front-end library manages the cache system automatically.
//TodoList parent Component
...
const [tasksIds, setTasksIds] = useState([]);
useEffect(() => {
if (user) {
return onValue(ref(database, `users/${user}/tasks`), (snapshot) => {
const todos = snapshot.val();
// Build an array of ids
const tasksIdsFromDb = todos.map((todo) => todo.id);
setTasksIds(tasksCopy);
});
} else {
setTasksIds([]);
}
}, [user]);
...
// keep the rest of the code and pass tasksIds instead of tasks to the flatlist
const Task = ({ taskId, memoizedOnPressPriority }) => {
const [task, setTask] = useState(null)
const { user } = useContext(AuthContext);
useEffect(() => {
if (user) {
// retrieve data by id so only the updated component will rerender
// I guess this will be something like this
return onValue(ref(database, `users/${user}/tasks/${taskId}`), (snapshot) => {
const todo = snapshot.val();
setTask(todo);
});
} else {
setTask(null);
}
}, [user]);
if (task === null) {
return null
}
// return the component like before

React Context value gets updated, but component doesn't re-render

This Codesandbox only has mobile styles as of now
I currently have a list of items being rendered based on their status.
Goal: When the user clicks on a nav button inside the modal, it updates the status type in context. Another component called SuggestionList consumes the context via useContext and renders out the items that are set to the new status.
Problem: The value in context is definitely being updated, but the SuggestionList component consuming the context is not re-rendering with a new list of items based on the status from context.
This seems to be a common problem:
Does new React Context API trigger re-renders?
React Context api - Consumer Does Not re-render after context changed
Component not re rendering when value from useContext is updated
I've tried a lot of suggestions from different posts, but I just cannot figure out why my SuggestionList component is not re-rendering upon value change in context. I'm hoping someone can give me some insight.
Context.js
// CONTEXT.JS
import { useState, createContext } from 'react';
export const RenderTypeContext = createContext();
export const RenderTypeProvider = ({ children }) => {
const [type, setType] = useState('suggestion');
const renderControls = {
type,
setType,
};
console.log(type); // logs out the new value, but does not cause a re-render in the SuggestionList component
return (
<RenderTypeContext.Provider value={renderControls}>
{children}
</RenderTypeContext.Provider>
);
};
SuggestionPage.jsx
// SuggestionPage.jsx
export const SuggestionsPage = () => {
return (
<>
<Header />
<FeedbackBar />
<RenderTypeProvider>
<SuggestionList />
</RenderTypeProvider>
</>
);
};
SuggestionList.jsx
// SuggestionList.jsx
import { RenderTypeContext } from '../../../../components/MobileModal/context';
export const SuggestionList = () => {
const retrievedRequests = useContext(RequestsContext);
const renderType = useContext(RenderTypeContext);
const { type } = renderType;
const renderedRequests = retrievedRequests.filter((req) => req.status === type);
return (
<main className={styles.container}>
{!renderedRequests.length && <EmptySuggestion />}
{renderedRequests.length &&
renderedRequests.map((request) => (
<Suggestion request={request} key={request.title} />
))}
</main>
);
};
Button.jsx
// Button.jsx
import { RenderTypeContext } from './context';
export const Button = ({ handleClick, activeButton, index, title }) => {
const tabRef = useRef();
const renderType = useContext(RenderTypeContext);
const { setType } = renderType;
useEffect(() => {
if (index === 0) {
tabRef.current.focus();
}
}, [index]);
return (
<button
className={`${styles.buttons} ${
activeButton === index && styles.activeButton
}`}
onClick={() => {
setType('planned');
handleClick(index);
}}
ref={index === 0 ? tabRef : null}
tabIndex="0"
>
{title}
</button>
);
};
Thanks
After a good night's rest, I finally solved it. It's amazing what you can miss when you're tired.
I didn't realize that I was placing the same provider as a child of itself. Once I removed the child provider, which was nested within itself, and raised the "parent" provider up the tree a little bit, everything started working.
So the issue wasn't that the component consuming the context wasn't updating, it was that my placement of providers was conflicting with each other. I lost track of my component tree. Dumb mistake.
The moral of the story, being tired can make you not see solutions. Get rest.

useState not updating[NOT AN ASYNC ISSUE]

I'm new to react hooks so I'm practicing with showing and hiding a div when checking and unckecking a checkbox input. The problem is that the state updates on the main file where I have the function that handles it but in the file where I actually have the div it does not update so it does not hide or display.
File that handles the change of the state:
import {react, useState} from "react";
export const Checker = () => {
const [checked, setChecked] = useState(true)
const clickHandler = () => {
setChecked(!checked)
console.log(checked)
}
return {checked, clickHandler, setChecked}
}
File where the checkbox is located:
import React from "react";
import { Extras, Wrapper } from "./extras.styles";
import { Checker } from "../hooks/useCheckboxes";
const Extra = () => {
const {checked, setChecked, clickHandler} = Checker()
return <>
<Extras>
<Wrapper>
<input type= 'checkbox' onClick={clickHandler} checked = {checked} onChange={e => setChecked(e.target.checked)}></input>
</Wrapper>
</Extras>
</>
}
export default Extra;
File that contains the div i want to display and hide dynamically:
import house from '../../icons/house.png'
import { Wrapper } from "./foto.styles";
import { Checker } from "../hooks/useCheckboxes";
import { Inside, Middle} from "./foto.styles";
const Home = () => {
const {checked} = Checker()
return <>
<Wrapper>
<Inside>
<Middle>
{checked && <House src={house}/>}
</Middle>
</Inside>
</Wrapper>
</>
}
export default Home;
Some issues are:
Checker looks like you want it to be a custom hook, not a React component, so it should be called useChecker or something like that, not Checker
You have both a change handler and a click handler. You should only have one. If you want the new state to come from the checkbox, you should use e.target.checked. If you want the new state to flip the old state, use the clickHandler you defined in Checker.
You only need a fragment when enclosing multiple elements. If you only have one, you don't need a fragment.
Because state setters don't update the variable immediately, your console.log(checked) won't display the new value, but the old value - if you want to log the new value when it changes, use useEffect with a dependency array of [checked] instead.
const Extra = () => {
const { checked, clickHandler } = useChecker()
return (
<Extras>
<Wrapper>
<input type='checkbox'checked={checked} onChange={clickHandler} />
</Wrapper>
</Extras>
)
}
use it like that
const {checked, clickHandler, setChecked} = Checker()
Or if you want to be able to make custom names then you need to use an array instead of an object.
the function return value.
return [checked, clickHandler, setChecked]
the function call
const [checked, setChecked, clickHandler] = Checker()
and for convention follow react hooks naming rules by renaming the function to useChecker() instead of Checker()

How to delete array of objects in react native using useState hook?

I am a beginner in react native. I am stuck as I am unable to create a button which calls the function delBlogPost which would delete the last element in array of blogPosts. I want to use useState hook to delete the element. It would be helpful if anyone can help me with this. Here is the code:
BlogContext.js
import React, {useState} from 'react';
const BlogContext = React.createContext();
export const BlogProvider = ({ children }) => {
const [blogPosts, setBlogPosts] = useState([]);
const addBlogPost = () => {
setBlogPosts([...blogPosts, {title: `Blog Post #${blogPosts.length+1}`}]);
};
{/* I want to create a delBlogPost function here which would delete the last element in array of objects*/}
const delBlogPost = () => {
};
return (
<BlogContext.Provider value={{data: blogPosts, addBlogPost, delBlogPost }}>
{children}
</BlogContext.Provider>
);
};
export default BlogContext;
This is where I am creating the button to delete. Just like add blogpost button I want to have delete post button using useState.
IndexScreen.js
import React, { useContext } from 'react';
import { View, StyleSheet, Text, FlatList, Button} from 'react-native';
import BlogContext from '../context/BlogContext';
const IndexScreen = () => {
const { data, addBlogPost, delBlogPost} = useContext(BlogContext);
return (
<View>
<Text>Index Screen</Text>
<Button title = "Add Post" onPress ={addBlogPost}/>
<Button title = "Remove Post" onPress ={delBlogPost}/>
<FlatList
data={data}
keyExtractor={(blogPost) => blogPost.title}
renderItem={({ item }) => {
return <Text>{item.title}</Text>;
}}
/>
</View>
);
};
const styles = StyleSheet.create({});
export default IndexScreen;
The useState hook returns an array of values. The first element is always the value while the second value is always the setter function. State values using useState are immutable and cannot be directly modified, which is why you are given a setter. With this in mind, your addBlogPost and delBlogPost code should look like the following.
const [blogPosts, setBlogPosts] = React.useState([])
// since state is immutable, we can only set the value and not directly modify it
// we use the ... operator to add the current posts and then tack the new one at
// the end.
const addBlogPost = (blogPostToAdd) => setBlogPost([ ...blogPosts, blogPostToAdd])
const delBlogPost = (blogPostToRemove) => {
const blogPostsWithRemoved = blogPosts.filter((blogPost) => {
// you do not have to use id to identify which blog post you are removing,
// but you have to use something to identify the blog post you want to remove
return blogPost.id !== blogPostToRemove.id
})
}
Now when you use these functions, you will want to actually pass the blog post object to them.
<Button onClick={(event) => {
addBlogPost(blogPost)
}}>
Click Me to Add
</Button>

ReactJS hooks useContext issue

I'm kind of to ReactJS and I'm trying to use useContext with hooks but I'm having some trouble. I've been reading through several articles but I could not understand it.
I understand its purpose, but I can't figure out how to make it work properly. If I'm correct, the purpose is to be able to avoid passing props down to every children and be able to access values from a common provider at any depth of the component tree. This includes functions and state values. Please correct me if I'm wrong.
I've been testing with the following files. This is the ManagerContext.js file:
import { createContext } from 'react';
const fn = (t) => {
console.log(t);
}
const ctx = createContext({
title: 'This is a title',
editing: false,
fn: fn,
})
let ManagerContext = ctx;
export default ManagerContext;
Then I have the LessonManager.js file which is used in my main application:
import React from 'react';
import LessonMenu from './LessonMenu.js';
export default function LessonManager() {
return (
<LessonMenu />
)
}
And finally the LessonMenu.js:
import React from 'react';
import 'rsuite/dist/styles/rsuite.min.css';
import ManagerContext from './ManagerContext.js';
export default function LessonMenu() {
const value = React.useContext(ManagerContext);
return (
<div>
<span>{value.title}</span>
<button
onClick={()=>value.fn('ciao')}
>click</button>
<button
onClick={()=>value.title = 'new title'}
>click</button>
</div>
)
}
In the LessonMenu.js file the onClick={()=>value.fn('ciao')} works but the onClick={()=>value.title = 'new title'} doesn't re render the component.
I know something is wrong, but can someone make it a bit clearer for me?
In order for rerendering to occur, some component somewhere must call setState. Your code doesn't do that, so no rendering happens.
The setup you've done for the ManagerContext creates a default value, but that's only going to get used if you don't render any ManagerContext.Provider in your component tree. That's what you're doing now, but it's almost certainly not what you want to. You'll want to have some component near the top of your tree render a ManagerContext.Provider. This component can will be where the state lives, and among the data it sends down will be a function or functions which set state, thus triggering rerendering:
export default function LessonManager() {
const [title, setTitle] = useState('SomeOtherTitle');
const [editing, setEditing] = useState(false);
const value = useMemo(() => {
return {
title,
setTitle,
editing,
setEditing,
log: (t) => console.log(t)
}
}, [title, editing]);
return (
<ManagerContext.Provider value={value} >
<LessonMenu />
</ManagerContext.Provider/>
)
}
// used like:
export default function LessonMenu() {
const value = React.useContext(ManagerContext);
return (
<div>
<span>{value.title}</span>
<button onClick={() => value.log('ciao')}>
click
</button>
<button onClick={() => value.setTitle('new title')}>
click
</button>
</div>
)
}

Resources