FullCalendar: Render dayHeaderContent prop everytime an Event Changes - reactjs

I use the dayHeaderContent prop from Full Calendar V5 to Inject certain extra content, Whenever I change or update an event and whenever the calendar re renders, the dayHeaderContent prop is being called and my new content is displayed on the top, It all works fine.
The issue comes when I update the event using the eventDrop and eventResize, The event card itself updates fine, but the dayHeaderContent on the top does not re render, until I manually re fetch the events or click the next page and come back to current page.
Is there a way I can make the dayHeaderContent render eveytime eventDrop and eventResize is used? I know I could just use the calendar refresh or refetch event source, But I cannot do that because I am supplying just one single event source static array of events, So I cannot use the refresh methods.
// Here's how I am supplying my events, here data is a static array
calendarRef.current.getApi().addEventSource(data)
// I want the below Inject function to run eveytime an event is update via eventDrop and eventResize props
const injectDayHeaderContent = (args) => {
if (args.view.type === 'timeGridWeek') {
return (
<span>
{convertDecimalToHHMM(showHours)}
</span>
)
}
}
return(
<FullCalendar
.....
.....
dayHeaderContent={injectDayHeaderContent}
/>
)

Related

How do I prevent unnecessary, repetitive side-effects from my React useEffect hook?

I am having trouble preventing unnecessary side-effects from a useEffect hook linked to a component that gets re-rendered/reused multiple times after being clicked. I want the side-effect to trigger an action once, but it is being triggered for every render.
I'm building a task viewer that displays nested task data. Please see screenshot #1 for reference.
For context, the main display shows the main tasks (see Event Data) and renders a clickable button if the task has sub-events. When this button is clicked, the selected main task is displayed at the top of the hierarchy view (see Event Hierarchy) and its sub-events are displayed below the hierarchy in a separate pane (see Event Details).
Like the main tasks, if these sub-events in 'Event Details' have their own sub-events, they are also rendered with a clickable button. When this sub-event button is clicked, this clicked sub-event is added to the bottom of the hierarchy, where the clicked main task is already displayed in bold. This selected sub-event's sub-events then replace the content in the 'Event Details' pane.
As the user clicks through the nested data, the clicked sub-event is added to the bottom of the hierarchy so that the user has an idea of where he is in the nested data and its sub-events displayed in 'Event Details'. All 'Event Hierarchy' and 'Event Details' data is cleared when the user selects a new main event or selects a new page.
The hierarchy events are held in an array managed via useState and every time another sub-event is clicked, it is added to this array. That's the idea, at least.
#1
My problem is this:
If I place my setHierarchy function inside a useEffect hook with the selectedTask as dependency, it renders the selectedTask in the hierarchy instantaneously, but the button component that triggers setHierarchy is re-rendered for every sub-event being displayed in 'Event Details' (as I want each event to be clickable) and in doing so, it adds that many copies of the event to my hierarchy array. This happens even though I am checking to see if the hierarchy array already contains the selected subevent before adding it. See result in screenshot #2.
I have tried various configurations of checking the array, but I cannot seem to stop it from adding these copies to and subsequently displaying them in the Hierarchy.
If I place the setHierarchy function inside my click handler, only one single event is added, but it executes before the selectedSubEvent has been updated. This means the hierarchy array is empty upon first render and stays one click 'behind' ie. a specific event is only displayed upon the following click event, after the click that selected it.
#2
This is all done inside my ExpandSubEvents button component (see code below) and also managed via a context provider.
I have tried moving the setHierarchy into a separate function, inside a useCallback, and triggering it from both the clickHandler and the useEffect that sets the selectedSubEvent. This did not resolve the issue.
I've also tried useRef to try and link it to the latest state. I'm not sure that's even doable/correct.
What am I doing wrong here? I am fairly new to coding, so any input on this would be much appreciated.
Sidenote: I suspect that my setup is perhaps beyond the intended scope of useContext. Is it? What can I do to make improvements? Is this perhaps in any way responsible for my issue?
Thank you for taking your time to read this far. I appreciate it!
Deon
ExpandSubEvents Component
import React, { useCallback, useContext, useEffect, useMemo } from 'react';
import SubEventContext from '../../store/sub-event-context';
import classes from './ExpandSubEvents.module.css';
const ExpandSubEvents: React.FC<{
id: number;
subEvents: number;
}> = React.memo((props) => {
// Extract context
const subEventCtx = useContext(SubEventContext);
const {
subEvents,
subEventParentId,
selectedSubEvent,
hierarchy,
setSubEventParentId,
setFetchId,
setSelectedSubEvent,
setHierarchy,
} = subEventCtx;
// Get id of event for when it is clicked
const id = React.useMemo(() => props.id, [props.id]);
let eventIds: number[] = useMemo(() => [], []);
if (hierarchy) {
for (const event of hierarchy) {
eventIds.push(event.id);
}
}
// Set CSS classes to style button if it has sub-events
let subEventQuantity = props.subEvents;
let importedClasses = `${classes['sub-event-button']}`;
if (subEventQuantity === 0) {
importedClasses = `${classes['no-sub-events']}`;
}
// Push the event to the Hierarchy display
// NOTE Tried moving the setHierarchy to a separate function, but it did not make a difference
// const triggerHierarchy = useCallback(() => {
// if (!eventIds.includes(id))
// setHierarchy((prevState) => [...prevState, ...selectedSubEvent]);
// }, [eventIds, id, selectedSubEvent, setHierarchy]);
// Respond to subevent button click event
const clickHandler = useCallback(() => {
setSubEventParentId(id);
setFetchId(id);
// This setHierarchy works, but executes before the selectedSubEVent has been updated
// Furthermore, if a new subevent is selected, it checks if the NEW clicked one has been added
// BUT sends the OLD event still in selectedSubEvent to the hierarchy before IT has been updated
// meaning that the check does not stop the same event being added twice
if (!eventIds.includes(id))
setHierarchy((prevState) => [...prevState, ...selectedSubEvent]);
}, [
eventIds,
id,
selectedSubEvent,
setFetchId,
setHierarchy,
setSubEventParentId,
]);
// NOTE Tried useRef to get setHierarchy to use the latest selectedSubEvent
// const subEventRef = useRef<Event[]>([]);
// subEventRef.current = hierarchy;
// Trying to setHierarchy directly from its own useEffect
// useEffect(() => {
// if (!eventIds.includes(id))
// setHierarchy((prevState) => [...prevState, ...selectedSubEvent]);
// }, [eventIds, hierarchy, id, selectedSubEvent, setHierarchy]);
// Filter the event from the subEvent array and set it to selectedSubEvent
useEffect(() => {
setSelectedSubEvent(
subEvents.filter((subEvent) => subEvent.id === subEventParentId)
);
}, [setSelectedSubEvent, subEventParentId, subEvents]);
return (
<button onClick={clickHandler} className={importedClasses}>
{subEventQuantity}
</button>
);
});
export default ExpandSubEvents;

How to reload a specific tag in reactJS

I want to reload the UserPlaylist tag after running the onChangeTracks() function in order to update it's contents but I'm not sure how to re-execute a specific tag if possible.
Parent Component:
render(){
return(
<li><UserPlaylist onChange={this.onChangeTracks}/></li>
);
}
UserPlaylist Component (Child Component):
render() {
return(
window.addEventListener('DOMContentLoaded', (event) => {
this.getPlaylists() //Have tracks load immediately
}),
<select value={"DEFAULT"} onChange={this.props.onChange}>
<option value="DEFAULT"> Add to Playlist </option>
<option value="new"> New Playlist </option>
{
this.state.users_playlists.map((playlist, index) => (
<option key={index} value={playlist.id}> { playlist.name }</option>
))
}
</select>
);
}
Components will rerender every time their state is updated. So you should update the state of your Parent component whenever the this.onChangeTracks function is invoked. Since this.state.currentTrack is being passed in as a prop to UserPlaylist, once it is updated via a call to setState the UserPlaylist component will receive new props and should rerender those new props accordingly.
Example:
onChangeTracks(val) {
...
this.setState({currentTrack: val});
...
}
EDIT
Here's a Codesandbox that updates the "New Playlist" select element whenever the onChange event is called.
Here's how it works: First, the useEffect hook fires and loads in data from the examplePlaylists variable. In your app, you should populate this with the data you have saved in your database or localStorage, etc. This data is passed to the playlists state which uses the useState hook. Whenever a person selects an option from the dropdown menu, the onChange event is fired and calls the handleUpdatePlaylists function. This function first prompts the user to enter the name of their new playlist. It then updates the playlists state through the updatePlaylistsfunction. Now, since the state has changed, the component will rerender and map over all the playlists, displaying their names in the dropdown menu. All without needing to refresh the page.
Of course, since I don't have a DB to save these to, the new playlists will disappear on page refresh. But you can write logic to save them however you wish.
And while this works, I think that you should look to using buttons and the onClick event instead of a select element to handle this logic. Since a new playlist is created whenever an option is selected, then clicking on the name of any playlist will also create them. This doesn't appear to be intended functionality.
So to summarize: handle updates to your components by changing their state. Once state changes, the components will rerender with their new state available for you to work with.

React: Append component animations on local and redux state updates

I am building a component that renders an animation on every local and redux state update. Local state is updated on a button click and it also makes an ajax request to a remote server to update that an user clicked the button. I update local state on every click like so:
const [selfAnimations, setAnimations] = useState<AnimationRenders>(initialAnimationState);
const onButtonClickCallback = () => {
makeRemoteAjaxCall();
selfAnimations.localEventsCount += 1;
selfAnimations.animationsToRender.push(Date.now);
setAnimations(selfAnimations);
};
I update the data coming in from remote server like this:
useEffect(() => {
if (animationDataCount && animationDataCount > 0) {
const {
updatedLocalEventsCount,
updatedAnimations,
} = filterDedupedLocalEvents(selfAnimations, animationDataCount);
setAnimations({
localEventsCount: updatedLocalEventsCount,
animationsToRender: updatedAnimations,
});
}
}, [animationDataCount]);
I render a div with a component I created:
<div classNames={...}>
<AnimationContainer numberOfAnimations={selfAnimations.animationsToRender.length} />
</div>
<div className={...}>
<MyButton ... />
</div>
On client side, data is received on regular intervals. My AnimationContainer component can render any number of received animations. However, everytime a user clicks the button or client recieves remote data it updates the state and drops previous animations. Looking online I could delay the rerender with css but the behavior I need is to append to existing animations on state updates. Is there a mechanism that I could use for that behavior?
I misunderstood how react renders. React does not necessarily create a new child component and unmount the old component on props/state updates. In my case, instead of storing counts of what needed to be rendered, I need to store distinct keys for what needed to be rendered, because even if the AnimationContainer component receives updates from props, if it only received counts, then it may not trigger rerenders because the count can be the same. I got around this by implementing the props of localEvents as an array of timestamps: number[]. By passing down timestamps to AnimationContainer and refactoring AnimationContainer to check props update and store into state of animations in middle of rendering and a queue of animations to render, I was able to create a component that can receive and continually renders animations based on events.

How can I make sure a React parent component loads data before its child component mounts?

I'm writing a simple calendar application that uses a common layout to wrap different views of events (month view shows a larger calendar with all the days of the month and events for each day, week view just shows a vertical list of events for that week, etc.). The common layout includes a calendar picker control for selecting the date, and then a list of event categories that can be checked or unchecked to show events relating to sports, entertainment, etc.
When the layout mounts, I'm calling an async Redux action creator to get the list of event categories from the database. When those are retrieved, they're saved in a Redux store with a property of Selected set to true, since they're all selected at initial load.
async componentWillMount() {
await this.props.getEventTypes();
}
When the month view, which is a child of the layout view, mounts, it's grabbing all the events for the given month. Part of the selection process of getting those events is sending the list of selected event categories to the backend so it only gets events from the selected categories.
async componentWillMount() {
await this.props.getWeeks();
}
The problem is, the selected categories list is always empty when the month view goes to grab the events for the month. So it's not going to select anything since no categories are selected.
It seems the only way this can be happening is if the child component is mounting first, or if the parent component is taking so long to get the event categories that the getWeeks process finishes first (this is unlikely as the process to grab the weeks and days and their events is much more involved than just selecting the event category list).
So, how can I make sure the parent component grabs the event categories from the database and puts them in the Redux store before the child component selects its events?
I know one way, probably the best way, to do this would be to have the list of event categories render into the page on the server side, so it's just already present at initial load. I'll probably end up doing it that way, but I'd also like to know how to do it all through client-side actions, in case I need to do it that way for some reason in the future.
You can try like this
Set isDataLoaded when data is available.
Use ternary operator for conditional rendering.
In you render
return(
<>
....
{ isDataLoaded ? <ChildComponent /> : null }
....other sutff
</>
);
Use can also use the && operator
return(
<>
....
{ isDataLoaded && <ChildComponent /> }
....other sutff
</>
);
You can integrate componentDidUpdate() and use it to render your child-components in a somewhat synchronous flow.
Let's say the structure of your Parent Component should look something like the following:
Parent
class Parent extends React.Component{
state = {
renderChildren: false
}
async componentDidMount() {
await this.props.getEventTypes();
}
componentDidUpdate(prevProps){
if(this.props.yourUpdatedReducer !== prevProps.yourUpdatedReducer){
this.setState({
renderChildren: true
})
}
}
render(){
const { renderChildren } = this.state
return(
{ renderChildren ? <Child/> : "Loading" }
)
}
}
You want a key in your state that determines whether you should
render the Child component.
In componentDidMount(), you call the action-creator function, when
it completes, you get updated props.
With updated props, you trigger componentDidUpdate(), where you
check the values in your reducer. If the values are
different that means you got the updated data from your database, so
everything has loaded.
Great, so now you want to mount your Child component, so you
update-state, setting renderChildren to true, thus re-rendering the
Parent component. Now Child gets rendered and should behave as expected.

Dynamically passing data to a child component in react

I have a react component that is responsible for listing out data and if the user clicks on a particular data item, it renders a popup with the data the user clicked passed to it. The popup is defined in a separate component but a single instance of the popup is defined in listing component as follows:
render(){
return(
...
{tasks.map((task, index) => {
return (
<p><a onClick={() => self.edit(task.id)}>{task.name}</a></p>
);
})}
<EditTaskPopup show={self.state.showEditPopup} onClose={self.onClosePopup} task={self.state.editData} />
...
)
}
The edit function, packages up the data and sets the component's state so that the data in included in the editData variable and the popup is shown as follows:
self.setState({showEditPopup: true, editData: tasks[x]});
This all works fine but my question is how I should correctly receive that data in the popup container, EditTaskPopup. The constructor of EditTaskPopup fire off when the parent component loads, so no user interaction has occurred, so no value is passed in. Same holds true for componentDidMount. I can see the correct value being passed in when the componentDidUpdate fires off, but that also fires off during the normal operation of the popup where I'm collecting information about what the user is typing within the popup and placing those values in state. Is there an event in a component that only fires off when a parent component changes the parameters passed into it but doesn't fire off when state changes within the component itself?
Try utilising lifecycle method: https://reactjs.org/docs/react-component.html#componentdidupdate

Resources