React: break dependency between 2 related contexts with top-level constant object - javascript

I'm working on a new major release for react-xarrows, and I came up with some messy situation.
It's not going to be simple to explain, so let's start with visualization:
consider the next example - 2 draggable boxes with an arrow drawn between them, and a wrapping context around them.
focused code:
<Xwrapper>
<DraggableBox box={box} />
<DraggableBox box={box2} />
<Xarrow start={'box1'} end={'box2'} {...xarrowProps} />
</Xwrapper>
Xwrapper is the context, DraggableBox and Xarrow are, well, you can guess.
My goal
I want to trigger a render on the arrow, and solely on the arrow, whenever one of the connected boxes renders.
My approach
I want to be able to rerender the arrow from the boxes, so I have to consume 'rerender arrow'(let's call it updateXarrow) function on the boxes, we can use a context and a useContext hook on the boxes to get this function.
I will call XelemContext to the boxes context.
also, I need to consume useContext on Xarrow because I want to cause a render on the arrow whenever I decide.
this must be 2 different contexts(so I could render xarrow solely). one on the boxes to consume 'updateXarrow', and a different context consumed on Xarrow to trigger the reredner.
so how can I pass this function from one context to another? well, I can't without making an infinite loop(or maybe I can but could not figure it out), so I used a local top-level object called updateRef.
// define a global object
const updateRef = { func: null };
const XarrowProvider = ({ children }) => {
// define updateXarrow here
...
// assign to updateRef.func
updateRef.func = updateXarrow;
return <XarrowContext.Provider value={updateXarrow}>{children}</XarrowContext.Provider>;
};
//now updateRef.func is defined because XelemProvider defined later
const XelemProvider = ({ children }) => {
return <XelemContext.Provider value={updateRef.func}>{children}</XelemContext.Provider>;
};
the thing is, that this object is not managed by react, and also, i will need to handle cases where there is multiple instances of Xwrapper, and I'm leaving the realm of React, so i have 2 main questions:
there is a better approach? maybe I can someone achieve my goal without going crazy?
if there is no better option, is this dangerous? I don't want to release a code that will break on edge cases on my lib consumer's apps.
Code
DraggableBox
const DraggableBox = ({ box }) => {
console.log('DraggableBox render', box.id);
const handleDrag = () => {
console.log('onDrag');
updateXarrow();
};
const updateXarrow = useXarrow();
return (
<Draggable onDrag={handleDrag} onStop={handleDrag}>
<div id={box.id} style={{ ...boxStyle, position: 'absolute', left: box.x, top: box.y }}>
{box.id}
</div>
</Draggable>
);
};
useXarrow
import React, { useContext, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { XelemContext } from './Xwrapper';
const useXarrow = () => {
const [, setRender] = useState({});
const reRender = () => setRender({});
const updateXarrow = useContext(XelemContext);
useLayoutEffect(() => {
updateXarrow();
});
return reRender;
};
export default useXarrow;
Xwrapper
import React, { useState } from 'react';
export const XelemContext = React.createContext(null as () => void);
export const XarrowContext = React.createContext(null as () => void);
const updateRef = { func: null };
const XarrowProvider = ({ children }) => {
console.log('XarrowProvider');
const [, setRender] = useState({});
const updateXarrow = () => setRender({});
updateRef.func = updateXarrow;
return <XarrowContext.Provider value={updateXarrow}>{children}</XarrowContext.Provider>;
};
const XelemProvider = ({ children }) => {
console.log('XelemProvider');
return <XelemContext.Provider value={updateRef.func}>{children}</XelemContext.Provider>;
};
const Xwrapper = ({ children }) => {
console.log('Xwrapper');
return (
<XarrowProvider>
<XelemProvider>{children}</XelemProvider>
</XarrowProvider>
);
};
export default Xwrapper;
const Xarrow: React.FC<xarrowPropsType> = (props: xarrowPropsType) => {
useContext(XarrowContext);
const svgRef = useRef(null);
....(more 1100 lines of code)
logs
I left some logs.
on drag event of a single box you will get:
onDrag
DraggableBox render box2
XarrowProvider
xarrow
Note
currently, this is working as expected.
Update
after many hours of testing, this seems to work perfectly fine. I manage my own object that remember the update function for each Xwrapper instance, and this breaks the dependency between the 2 contexts. I will leave this post in case someone else will also come across this issue.
Update (bad one)
this architecture breaks on react-trees with <React.StrictMode>...</React.StrictMode> :cry:
any idea why? any other ideas ?

just in case someone would need something similar: here's a version that will work even with react strictmode(basically being rellyed of effect which called once and not renders):
import React, { FC, useEffect, useRef, useState } from 'react';
export const XelemContext = React.createContext(null as () => void);
export const XarrowContext = React.createContext(null as () => void);
// will hold a object of ids:references to updateXarrow functions of different Xwrapper instances over time
const updateRef = {};
let updateRefCount = 0;
const XarrowProvider: FC<{ instanceCount: React.MutableRefObject<number> }> = ({ children, instanceCount }) => {
const [, setRender] = useState({});
const updateXarrow = () => setRender({});
useEffect(() => {
instanceCount.current = updateRefCount; // so this instance would know what is id
updateRef[instanceCount.current] = updateXarrow;
}, []);
// log('XarrowProvider', updateRefCount);
return <XarrowContext.Provider value={updateXarrow}>{children}</XarrowContext.Provider>;
};
// renders only once and should always provide the right update function
const XelemProvider = ({ children, instanceCount }) => {
return <XelemContext.Provider value={updateRef[instanceCount.current]}>{children}</XelemContext.Provider>;
};
const Xwrapper = ({ children }) => {
console.log('wrapper here!');
const instanceCount = useRef(updateRefCount);
const [, setRender] = useState({});
useEffect(() => {
updateRefCount++;
setRender({});
return () => {
delete updateRef[instanceCount.current];
};
}, []);
return (
<XelemProvider instanceCount={instanceCount}>
<XarrowProvider instanceCount={instanceCount}>{children}</XarrowProvider>
</XelemProvider>
);
};
export default Xwrapper;

Related

Why is simple useState initialization is not working?

I have this React code:
import React, { useState, useEffect } from "react";
import axios from "axios";
function App() {
const [players, setPlayers] = useState([]);
// Get all Players
const getAllPlayersUrl = "http://localhost:5087/api/GetAllPlayers";
useEffect(() => {
axios.get(getAllPlayersUrl).then((response) => {
setPlayers(response.data);
});
}, []);
const [playerCount, setPlayerCount] = useState(players.length);
return (
<div>
<p>{`This is how many there are: ${playerCount}`}</p>
</div>
);
}
export default App;
I want to print how many initial players using playerCount variable. However it says it's zero:
This is how many there are: 0
If I instead print players.length, it would output the correct number:
<p>{`This is how many there are: ${players.length}`}</p>
This is how many there are: 9
Even if I remove dependency array to keep rendering, playerCount still wont update:
useEffect(() => {
axios.get(getAllPlayersUrl).then((response) => {
setPlayers(response.data);
});
});
I wonder why the useState is not working? Is there something I am missing in my code?
A good rule of thumb with state (and props) is to avoid duplicating state values when a value can be determined entirely by another. Otherwise, you can run into issues like these, where keeping multiple states in sync can be more challenging than it needs to be.
Here, you set the initial value of playerCount when the component mounts:
const [playerCount, setPlayerCount] = useState(players.length);
And the component mounts only once - and at that time, players is the empty array - so playerCount becomes 0, and because you never call setPlayerCount, it always remains 0.
While you could fix it by calling setPlayerCount inside your .then, a better approach would be to either calculate the player count from the players state only when needed:
function App() {
const [players, setPlayers] = useState([]);
const getAllPlayersUrl = "http://localhost:5087/api/GetAllPlayers";
useEffect(() => {
axios.get(getAllPlayersUrl).then((response) => {
setPlayers(response.data);
});
}, []);
return (
<div>
<p>{`This is how many there are: ${players.length}`}</p>
</div>
);
}
Or, if you really had to, to memoize the count depending on the players array (without creating additional state).
function App() {
const [players, setPlayers] = useState([]);
const playerCount = useMemo(() => players.length, [players]);
const getAllPlayersUrl = "http://localhost:5087/api/GetAllPlayers";
useEffect(() => {
axios.get(getAllPlayersUrl).then((response) => {
setPlayers(response.data);
});
}, []);
return (
<div>
<p>{`This is how many there are: ${playerCount}`}</p>
</div>
);
}

Connecting two contexts return undefined values in the consumer context react hooks

I really do not understand why this is not working, basically, I have a header component with its own context. On the other hand, I have a popOver component that goes inside the header, and this popOver also has its own context.
Now, there is a list of elements that are rendered inside the popOver, the user picks which elements to render, and such list needs to be rendered simultaneously in the header, for that reason I am trying to keep both contexts synchronized, the problem appears when I try to consume the header context inside the popOver context, the values consumed appear to be undefined.
const HeaderContext = createContext();
export const HeaderProvider = ({ children }) => {
const [headChipList, setHeadChipList] = useState([]);
const [isChipOpen, setIsChipOpen] = useState(false);
useEffect(() => {
if (headChipList.length) setIsChipOpen(true);
}, [headChipList]);
return (
<HeaderContext.Provider value={{ headChipList, setHeadChipList, isChipOpen, setIsChipOpen }}>
{children}
</HeaderContext.Provider>
);
};
export const useHeaderContext = () => {
const context = useContext(HeaderContext);
if (!context) throw new Error('useHeaderContext must be used within a HeaderProvider');
return context;
};
As you can see at the end there's a custom hook that allows an easier consumption of the context and also is a safeguard in case the custom hook is called outside context, the popOver context follows this same pattern:
import React, { useState, useContext, createContext, useEffect } from 'react';
import { useHeaderContext } from '(...)/HeaderProvider';
const PopoverContext = createContext();
export const PopoverProvider = ({ children }) => {
const { setHeadChipList, headChipList } = useHeaderContext; // this guys are undefined
const [menuValue, setMenuValue] = useState('Locations with Work Phases');
const [parentId, setParentId] = useState('');
const [chipList, setChipList] = useState([]);
const [locations, setLocations] = useState([]);
useEffect(() => setChipList([...headChipList]), [headChipList]);
useEffect(() => setHeadChipList([...chipList]), [chipList, setHeadChipList]);
return (
<PopoverContext.Provider
value={{
menuValue,
setMenuValue,
chipList,
setChipList,
parentId,
setParentId,
locations,
setLocations
}}
>
{children}
</PopoverContext.Provider>
);
};
export const usePopover = () => {
const context = useContext(PopoverContext);
if (!context) throw new Error('usePopover must be used within a PopoverProvider');
return context;
};
I would really appreciate any highlight about this error, hopefully, I will be able to learn how to avoid this type of error in the future
You're not calling the useHeaderContext function. In PopoverProvider, change the line to
const { setHeadChipList, headChipList } = useHeaderContext();

Separate wrapper with api request in React

I am using React. Tell me how to make it beautifully (right!). On the page, I have two almost identical sections:
And I'm trying to follow the rule, keep containers and components separate. There is a wrapper in which there is one api request to receive a picture (hereinafter it is transmitted as a props) for a specific section, it is rendered in this way:
It turns out that this wrapper is (almost) the same:
I understand that this can be done correctly, but something does not work. I am confused by the fact that it is necessary to return two different components from the wrapper, where the api request to receive a picture goes. (I was looking towards hoc, but I haven't figured out how to use it myself). Thank you in advance.
I did it all the same through hoc. Here is the component itself:
function LoadingSnapshotHOC(Component) {
const NewComponent = (props) => {
const isMounted = useIsMounted();
const state = useSelector(({ dateParams }) => {
const { currentPage } = props;
return {
selectedTimeLabel: dateParams?.[currentPage].selectedTimePeriod.label,
compareTimeLabel: dateParams?.[currentPage].compareTimePeriod.label,
};
});
const [snapshot, setSnapshot] = useState("");
const updateSnapshot = async (deviceID) => {
const img = await getSnapshot(deviceID);
img.onload = () => {
if (isMounted.current) {
setSnapshot(img);
}
};
};
useEffect(() => {
if (props.deviceID) updateSnapshot(props.deviceID);
}, [props.deviceID]);
return (
<Component
{...props}
snapshot={snapshot}
selectedTimeLabel={state.selectedTimeLabel}
compareTimeLabel={state.compareTimeLabel}
/>
);
};
return NewComponent;
}
export default LoadingSnapshotHOC;
Next, I wrapped my components:
function HeatMapSnapshot({...}) {
...
}
export default LoadingSnapshotHOC(HeatMapSnapshot);
and
function TrafficFlowSnapshot({...}) {
...
}
export default LoadingSnapshotHOC(TrafficFlowSnapshot);
And their render. Thank you all for your attention!

Custom React hook, infinite loop only if I add the second dependency. Bug or something I can't understand?

I've made a really simple React hook. That's something seen on many guides and websites:
import { useEffect, useState } from 'react';
import axios from 'axios';
export const useFetchRemote = (remote, options, initialDataState) => {
const [data, setData] = useState(initialDataState);
useEffect(() => {
const fetchData = async () => {
const result = await axios.get(remote, options);
setData(result.data);
};
fetchData();
}, [remote]);
return data;
};
Example usage:
import { useFetchRemote } from '../utils';
export const UserList = () => {
const users = useFetchRemote('/api/users', {}, []);
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>}
</ul>
);
}
This is working. If I understand correctly:
With no dependencies like useEffect(() => { /*...*/ }), setting the state into the function would trigger a re-render, calling useEffect again, in an infinite loop.
With empty dependencies like useEffect(() => { /*...*/ }, []), my function will be called only the "very first time" component is mounted.
So, in my case, remote is a dependency. My function should be called again if remote changes. This is true also for options. If I add also options, the infinite loop starts. I can't understand... why this is happening?
export const useFetchRemote = (remote, options, initialDataState) => {
// ...
useEffect(() => {
// ...
}, [remote, options]);
// ...
};
The infinite loop is caused by the fact that your options parameter is an object literal, which creates a new reference on every render of UserList. Either create a constant reference by defining a constant outside the scope of UserList like this:
const options = {};
const initialDataState = [];
export const UserList = () => {
// or for variable options instead...
// const [options, setOptions] = useState({});
const users = useFetchRemote('/api/users', options, initialDataState);
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>}
</ul>
);
}
or if you intend the options parameter to be effectively constant for each usage of the userFetchRemote() hook, you can do the equivalent of initializing props into state and prevent the reference from updating on every render:
export const useFetchRemote = (remote, options, initialDataState) => {
const [optionsState] = useState(options);
const [data, setData] = useState(initialDataState);
useEffect(() => {
const fetchData = async () => {
const result = await axios.get(remote, optionsState);
setData(result.data);
};
fetchData();
}, [remote, optionsState]);
// ---------^
return data;
};
This second approach will prevent a new fetch from occuring though, if the options are dynamically changed on a particular call site of useFetchRemote().

How to Edit text function in React Hooks CRUD app? example note taking / todo app?

I'm learning React with Hooks and context. Learning by making a simple CRUD app. I have functions that allow me to ADD notes, DELETE notes, Filter if important, Toggle importance in some Context. So far so good.
However I'm stuck on how I would be able to edit my note. I've searched online and every react hooks CRUD tutorial seems to just be add and delete.
My functions for a CRUD note taking app below.
import React, { createContext, useState, useEffect } from 'react';
import uuid from 'uuid/v1';
export const NotesContext = createContext();
const NotesContextProvider = (props) => {
const [notes, setNotes] = useState([]);
const [showAll, setShowAll] = useState(true);
const notesToShow = showAll ? notes : notes.filter((note) => note.important);
//CRUD operations
// Create
const addNote = (content) => {
setNotes([
...notes,
{
content,
id: uuid(),
important: false
}
]);
};
// Update
const toggleImportance = (noteId) => {
const updatedNotes = notes.map((note) =>
note.id === noteId ? { ...note, important: !note.important } : note
);
setNotes(updatedNotes);
};
// Delete
const removeNote = (id) => {
setNotes(notes.filter((note) => note.id !== id));
};
// Get any notes stored in local storage
useEffect(() => {
const data = localStorage.getItem('notes');
if (data) {
setNotes(JSON.parse(data));
}
}, []);
// save notes to local storage
useEffect(() => {
localStorage.setItem('notes', JSON.stringify(notes));
}, [notes]);
return (
<NotesContext.Provider
value={{
addNote,
removeNote,
notes,
showAll,
setShowAll,
notesToShow,
toggleImportance
}}>
{props.children}
</NotesContext.Provider>
);
};
export default NotesContextProvider;
I'm hoping to have an onClick on an edit button on my note/todo and it'll allow me to update the note/todo.
Thanks any guidance would be appreciated.

Categories