Conditional Rendering of Arrays in React: For vs Map - javascript

I'm new to React and building a calendar application. While playing around with state to try understand it better, I noticed that my 'remove booking' function required a state update for it to work, while my 'add booking' function worked perfectly without state.
Remove bookings: requires state to work
const [timeslots, setTimeslots] = useState(slots);
const removeBookings = (bookingid) => {
let newSlots = [...timeslots];
delete newSlots[bookingid].bookedWith;
setTimeslots(newSlots);
}
Add bookings: does not require state to work
const addBookings = (slotid, tutorName) => {
timeslots[slotid].bookedWith = tutorName;
}
I think that this is because of how my timeslot components are rendered. Each slot is rendered from an item of an array through .map(), as most tutorials online suggest is the best way to render components from an array.
timeslots.map(slot => {
if (!slot.bookedWith) {
return <EmptyTimeslot [...props / logic] />
} else {
return <BookedTimeslot [...props / logic]/>
}
})
So, with each EmptyTimeslot, the data for a BookedTimeslot is available as well. That's why state is not required for my add bookings function (emptyTimeslot -> bookedTimeslot). However, removing a booking (bookedTimeslot -> emptyTimeslot) requires a rerender of the slots, since the code cannot 'flow upwards'.
There are a lot of slots that have to be rendered each time. My question is therefore, instead of mapping each slot (with both and information present in each slot), would it be more efficient to use a for loop to only render the relevant slot, rather than the information for both slots? This I assume would require state to be used for both the add booking and remove booking function. Like this:
for (let i=0;i<timeslots.length;i++) {
if (!timeslot[i].bookedWith) {
return <EmptyTimeslot />
} else {
return <BookedTimeslot />
}
}
Hope that makes sense. Thank you for any help.

Your addBooking function is bad. Even if it seems to "work", you should not be mutating your state values. You should be using a state setter function to update them, which is what you are doing in removeBookings.
My question is therefore, instead of mapping each slot (with both and information present in each slot), would it be more efficient to use a for loop to only render the relevant slot, rather than the information for both slots?
Your map approach is not rendering both. For each slot, it uses an if statement to return one component or the other depending on whether the slot is booked. I'm not sure how the for loop you're proposing would even work here. It would just return before the first iteration completed.
This I assume would require state to be used for both the add booking and remove booking function.
You should be using setTimeslots for all timeslot state updates and you should not be mutating your state values. That is true no matter how you render them.

Related

Why should i create a copy of the old array before changing it using useState in react?

Why this works
const handleToggle = (id) => {
const newTodos = [...todos]
newTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
});
setTodos(newTodos);
}
And this doesnt
const handleToggle = (id) => {
setTodos(prevTodos => prevTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
}))
}
Why do i have to create a copy of the old todos array if i want to change some item inside it?
You are doing a copy of the array in both cases, and in both cases you are mutating the state directly which should be avoided. In the second version you also forgot to actually return the todo, so you will get an array of undefined.
Instead you should shallow copy the todo you want to update.
const handleToggle = (id) => {
setTodos(prevTodos => prevTodos.map(todo => {
if (todo.id === id) {
return {...todo, completed: !todo.completed}
}
return todo
}))
}
Why mutating state is not recommanded? source
Debugging: If you use console.log and don’t mutate state, your past
logs won’t get clobbered by the more recent state changes. So you can
clearly see how state has changed between renders.
Optimizations: Common React optimization strategies rely on skipping
work if previous props or state are the same as the next ones. If you
never mutate state, it is very fast to check whether there were any
changes. If prevObj === obj, you can be sure that nothing could have
changed inside of it.
New Features: The new React features we’re
building rely on state being treated like a snapshot. If you’re
mutating past versions of state, that may prevent you from using the
new features.
Requirement Changes: Some application features, like implementing
Undo/Redo, showing a history of changes, or letting the user reset a
form to earlier values, are easier to do when nothing is mutated. This
is because you can keep past copies of state in memory, and reuse them
when appropriate. If you start with a mutative approach, features like
this can be difficult to add later on.
Simpler Implementation: Because
React does not rely on mutation, it does not need to do anything
special with your objects. It does not need to hijack their
properties, always wrap them into Proxies, or do other work at
initialization as many “reactive” solutions do. This is also why React
lets you put any object into state—no matter how large—without
additional performance or correctness pitfalls.
In practice, you can often “get away” with mutating state in React,
but we strongly advise you not to do that so that you can use new
React features developed with this approach in mind. Future
contributors and perhaps even your future self will thank you!
when you're changing an object using a hook correctly (- in this case useState) you are causing a re-render of the component.
if you are changing the object directly through direct access to the value itself and not the setter function - the component will not re-render and it will likely cause a bug.
and the .map(function(...)=>{..}) is a supposed to return an array by the items that you are returning from the function within. since you're not returning anything in the second example - each item of the array will be undefined - hence you'll have an array of the same length and all the items within will be undefined.
these kinds of bugs will not happen if you remember how to use array functions and react hooks correctly,
it's usually really small things that will make you waste hours on end,
I'd really recommend reading the documentation.

Update context when state object mutates

I have a PageContext that is holding the state of User objects as array. Each User object contains a ScheduledPost object that does mutate when user decides to add a new post. I have no idea how to trigger an update on my PageContext when it happens (I want to avoid forceUpdate() call). I need to somehow be notified of that, in order to re-render posts, maintain timer etc.
Please, see the code:
class User {
name: string;
createTime: number;
scheduledPosts: ScheduledPost[] = [];
/*
* Creates a new scheduled post
*/
public createScheduledPost(title : string, content : string, date : number): void {
this.scheduledPosts.push(Object.assign(new ScheduledPost(), {
title,
content,
date
}));
}
}
class ScheduledPost {
title: string;
content: string;
date: number;
public load(): void {
// Create timers etc.
}
public publish(): void {
// Publish post
}
}
// PageContext/index.tsx
export default React.createContext({
users: [],
editingUser: null,
setEditingUser: (user: User) => {}
});
// PageContextProvider.tsx
const PageContextProvider: React.FC = props => {
const [users, setUsers] = useState<User[]>([]);
const [editingUser, setEditingUser] = useState<User>(null);
// Load users
useEffect(() => {
db.getUsers()
.then(result => setUsers(result));
}, []);
return (
<PageContext.Provider value={{
users,
editingUser,
setEditingUser
}}>
{props.children}
</PageContext.Provider>
);
};
What I would like to achieve is, when consuming my provider with useContext hook:
const ctx = useContext(PageContext);
I would like to create a schedule post from any component like so:
// Schedule post (1 hour)
ctx.editingUser.createScheduledPost("My post title", "The post content", (new Date).getTime() + 1 * 60 * 60);
However, this wont work, since React doesn't know that User property has just mutated.
Questions:
How can I make React being notified of the changes within any of the User object instance? What is the way to solve it properly (excluding forceUpdate)?
Am I doing it right? I'm new to React and I feel like the structure I'm using here is cumbersome and just not right.
Where are the users being mutated? If you're storing them in your state as it appears, the changes should be detected. However if you're using the methods built into the User class to let them directly update themselves, then React will not pick up on them. You would need to update the entire users array in your state to make sure React can respond to the changes.
It's tough to give a more specific example without seeing exactly where/how you're updating your users currently, but a generalized mutation might go something like this (you can still use a class method, if desired):
const newUsers = Array.from(users); // Deep copy users array to prevent direct state mutation.
newUsers[someIndex].createScheduledPost(myTitle, myContent, myDate);
setUsers(newUsers); // Calling the setX function tied to your useState call will automatically trigger updates/re-renders for all (unless otherwise specified) components/operations that depend on it
In React re-render is caused by calling setState within component (or by using hooks, but the point is that you need to call specific method) or by changing component props. That means that manual mutation of your state will never cause a re-render - even if you had simple component and called
this.state.something = somethingElse;
re-render would not occur. Same thing works for context.
For your case, this means that you should not mutate editingUser state, but call setEditingUser with changed user state, something like:
const user = { ...ctx.editingUser };
user.createScheduledPost("My post title", "The post content", (new Date).getTime() + 1 * 60 * 60);
ctx.setEditingUser(user);
I'm not sure about your inner structure, but if that same user is also in users array, then you'll need to update that part of state by calling setUsers method where you maintain whole array and only update that single user which changed data - if thats the case then I'd think about restructuring the app because it already gets complicated for such simple state changes. You should also consider using redux, mobx or some other state management library instead of react context (my personal advice).
EDIT
Please take a look at this quite:
In a typical React application, data is passed top-down (parent to
child) via props, but this can be cumbersome for certain types of
props (e.g. locale preference, UI theme) that are required by many
components within an application. Context provides a way to share
values like these between components without having to explicitly pass
a prop through every level of the tree.
As you can see, react team is suggesting using context for some global preferences that are required within many components. The main problem with using context (in my opinion) is that you don't write natural react components - they don't receive dependant data through props but rather from within the context api itself. This means that you won't be able to reuse your components without also integrating context part of application.
While for example redux has similar concept of keeping state at one place, it still propagades that state (and its changes) to components via props, making your components undependent of both redux, context or anything else.
You can stick to react context and make whole app work with it, but I'm just saying it wouldn't be best practice to do so.

Alter react component state properly

I'm working at a project in which I have to display graphs.
For displaying graphs I'm using vis.js in particular react-vis-network a implementation for using parts of vis.js in React with its stateful approaches.
Initial nodes and edges are loaded before my component is mounted and are passed as props for an initial state.
I attached two eventHandler one direct to a vis.js (the underlying DOM library) and the other at a decorator (button).
The desired/expected behaviour:
A node is removed by clicking either the node or the corresponding button.
Observed behavior:
Sometimes a node is removed and sometimes a node just disappears for a few ms and is reattached but without a decorator/button.
I already tried to start with an empty state and attaching the nodes,edges in componentDidMount() but I got the same result. I hope you can give me a hint.
BTW: Is the way I use to attach components a/the right way?
Every other help to improve my class is appreciated also
class MyNetwork extends Component {
constructor(props){
super(props);
let componentNodes = [];
for (let node of props.nodes){
componentNodes.push(this.createNode(node));
}
let componentEdges = [];
for (let edge of props.edges){
componentEdges.push(this.createEdge(edge));
}
this.state = {nodes:componentNodes,edges:componentEdges};
["_handleButtonClick"].forEach(name => {
this[name] = this[name].bind(this);
});
}
createNode(node){
const Decorator = props => {
return (
<button
onClick={() =>{this._handleButtonClick(props);}}
>
Click Me
</button>
);
};
node.decorator = Decorator;
return React.createElement(Node,{...node})
}
createEdge(edge){
return React.createElement(Edge,{...edge})
}
addNode(node){
this.setState({
nodes: [...this.state.nodes, this.createNode(node)]
})
}
_handleButtonClick(e) {
if(e){
console.log("clicked node has id:" +e.id);
this.removeNode(e.id);
}
}
onSelectNode(params){
console.log(params);
window.myApp.removeNode(params[0]);
}
removeNode(id) {
let array = [...this.state.nodes]; // make a separate copy of the array
let index = array.findIndex(i => i.props.id === id );
array.splice(index, 1);
this.setState({nodes: array});
}
render() {
return (
<div id='network'>
<Network options={this.props.options} onSelectNode={this.onSelectNode}>
{[this.state.nodes]}
{[this.state.edges]}
</Network>
</div>
);
}
}
export default MyNetwork
Before clicking node 2
After clicking node 2
Update 1
I created a live example at stackblitz which isn't working yet caused by other failures I make and can't find.
The components I use are:
Network
Node
Edge
Edge and Node are extending Module
I reworked my MyNetwork component according to some mistakes xadm mentioned.
Components (espacially dynamic) shouldn't be stored in state.
I implemented two new functions nodes() and edges() // line 15-41*
key prop should be used, too.
key is used now // line 18 + 32*
Passed props cannot be modified, you still have to copy initial data
into state. State is required for updates/rerendering.
line 9*
*line numbers in live example I mentioned above
Update 2
I reworked my code and now the life sample is working.
My hope is that I could use the native vis.js events and use them in MyNetwork or other Components I will write.
I read about using 3rd Party DOM event in this question can't figure out to adapt it for my particular case. Because I don't know how to attach the event handler to . Is this possible to do so I can use the event in other components?
Or should I open another question for this topic?
I see several possibilities of problems here.
<Decorator/> should be defined outside of <MyNetwork /> class. Click handler should be passed as prop.
Components (espacially dynamic) shouldn't be stored in state. Just render them in render or by rendering method (called from render). Use <Node/> components with decorator prop, key prop should be used, too.
Passed props cannot be modified, you still have to copy initial data into state. State is required for updates/rerendering. You probably need to remove edge(-es) while removing node.
Create a working example (on stackblitz?) if a problem won't be resolved.
It sounds like React is re-initializing your component when you are clicking a button. Maybe someone smarter than I am can figure out why that is happening...
But since no one has commented on this yet, one way I have handled these sorts of issues is to take the state management out of the display component. You say you are passing the nodes and edges via props from a parent component. You might consider moving the addNode, removeNode, createEdge, and other methods up to the parent component so that it is maintaining the state of the node/edge structure and your display component <MyNetwork/> is only displaying what it receives as props.
Perhaps this isn't an option in your app, but I generally use Redux to remove the state management from the components all together. I find it reduces situations like this where "who should own the state" isn't always clear.

React Component Flickers on Rerender

I'm pretty new to react and have been working on this new page for work. Basically, there's a panel with filter options which lets you filter objects by color. Everything works but I'm noticing the entire filter panel flickers when you select a filter.
Here are the areas functions in the filter component I think bear directly on the filter and then the parent component they're inserted into. When I had originally written this, the filter component was also calling re render but I've since refactored so that the parent handles all of that - it was causing other problems with the page's pagination functionality. naturally. and I think that's kind of my problem. the whole thing is getting passed in then rerendered. but I have no idea how to fix it. or what's best.
checks whether previous props are different from props coming in from parent and if so, creates copy of new state, calls a render method with those options. at this point, still in child component.
componentDidUpdate(prevProps, prevState) {
if (prevState.selectedColorKeys.length !== this.state.selectedColorKeys.length ||
prevState.hasMatchingInvitation !== this.state.hasMatchingInvitation) {
const options = Object.assign({}, {
hasMatchingInvitation: this.state.hasMatchingInvitation,
selectedColorKeys: this.state.selectedColorKeys
});
this.props.onFilterChange(options);
}
}
handles active classes and blocks user from selecting same filter twice
isColorSelected(color) {
return this.state.selectedColorKeys.indexOf(color) > -1;
}
calls to remove filter with color name so users can deselect with same filter button or if its a new filter, sets state by adding the color to the array of selected color keys
filterByColor(color) {
if (this.isColorSelected(color.color_name)) {
this.removeFilter(color.color_name);
return;
}
this.setState({
selectedColorKeys:
this.state.selectedColorKeys.concat([color.color_name])
});
}
creating the color panel itself
// color panel
colorOptions.map(color => (
colorPicker.push(
(<li className={['color-filter', this.isColorSelected(color.color_name) ? 'active' : null].join(' ')} key={color.key} ><span className={color.key} onClick={() => { this.filterByColor(color); }} /></li>)
)
));
parent component
callback referencing the filter child with the onFilterChange function
<ThemesFilter onFilterChange={this.onFilterChange} />
onFilterChange(filters) {
const { filterThemes, loadThemes, unloadThemes } = this.props;
unloadThemes();
this.setState({
filterOptions: filters,
offset: 0
}, () => {
filterThemes(this.state.filterOptions.selectedColorKeys, this.state.filterOptions.hasMatchingInvitation);
loadThemes(this.state.offset);
});
}
when I place break points, the general flow seems to be :
filterByColor is triggered in event handler passing in that color
active classes are added to the color, a filter tag for that color is generated and appended
componentDidMount takes in the previous props/state and compares it to the new props/state. if they don't match, i.e state has changed, it creates a copy of that object, assigning the new states of what's changed. passes that as props to onFilterChange, a function in the parent, with those options.
onFilterChange takes those options, calls the action method for getting new themes (the filtering actually happens in the backend, all I really ever need to do is update the page) and passes those forward. its also setting offset to 0, that's for the page's pagination functionality.
It looks like the problem might be around the componentDidUpdate function which, after setting breakpoints and watching it go through the steps from filterByColor to componentDidMount, that componentDidMount loops through twice, checking again if the colorIsSelected, and throughout all that the color panel pauses to re-render and you get a flicker.
Is it possible creating the copy is causing it? since it's being treated, essentially, as a new object that isColorSelected feels necessary to double check? any help you guys have would be much appreciated, this shit is so far over my head I can't see the sun.
Can you change
componentDidUpdate(prevProps, prevState)
with
componentWillUpdate(nextProps, nextState)

Structuring my React/Redux app for efficiency

Im building an audio workstation app that will display a table of tracks containing clips. Right now I have a table reducer which returns a table object. The table object contains track objects and the track objects contain clip objects. I have a TableContainer which subscribes to the table store. My issue is I believe my app will be inefficient because it will re render the page every time a clip is added or manipulated. In reality only the particular track in which the clip resides would need to be re rendered right? How can I structure my app so not every little change re renders the entire app?
In the mapStateToProps of any component, don't select parent objects as a whole to send to the component. If possible select specific properties all the way to the leaf values. If your TableContainer's render() itself doesn't use the tracks array them make sure only the sibling properties that you do use get passed.
So instead of:
function mapStateToProps(state, props) {
return {
table: state.tables[props.tableId];
}
}
Do:
function mapStateToProps(state, props) {
let table = state.tables[props.tableId];
return {
name: table.name,
type: table.type
};
This allows React Redux to be more discerning when it comes to determining whether your component needs to be rerendered. It will see that even though the table had changed due to a clip change, neither the name, nor the type has changed.
However, since your Tablecomponent likely renders the Track components as well, you're likely not going to be able to avoid render calls. If any property anywhere up the tree gets altered, the tracks array also gets altered.
The solution in this case is to have the tracksarray not contain the entire track object but instead only a list of track IDs. You can then store the tracks alongside the tables and a change in one won't affect the other. Note that this only works if you do not go and fetch and pass the track object in the mapStateToProps of the Table component. You should make theTrack component in such a way that it accepts its ID instead of the actual object as a prop. This way the Table component is not dependent on the contents of the tracks at all.
The power of react is to re-render only what needs to be (by using the virtual DOM to make the comparison and the shouldComponentUpdate function).
I wouldn't look too much into it before it becomes a performance problem.
If it does, I would store the tracks in a separate directory and don't pass it to the app (main) component. In your Clip component's mapStateToProps function (if you use react-redux), fetch the track from the state as you get it's name from the props. This way if the track changes a lot (because of async fetching of slices for example), only the component will update.

Categories

Resources