React fast global redux-like variable - javascript

I'm making a front-end application using react/webgl and I need to be vary of performance improvements since almost everything must be rendered real-time and dynamically.
I need to render something on a canvas and need to use some variable's globally across many different components, but they need to be updated fast. Technically redux is what I need, however accessing dispatched variables takes time and causes crucial performance issues.
So instead I opted in to use useRef() which solves the “slow” issue but now I cannot update it’s value across different components. Using useRef() solves my issue but since it's not globally accessible it causes problems on other parts of the application.
Declaration of the variable looks like this:
import { useRef } from 'react';
const WebGLStarter = (props) => {
...
const myValue = useRef();
myValue.current = someCalculation();
function render(myValue.current){
...
requestAnimationFrame(function () {
render(myValue.current);
});
}
...
}
Currently someCalculation() is on the same component as it's declaration. I want to use someCalculation() on a different file but I can't do it beacuse useRef() won't allow me to. And again, I can't use redux because it's slow.
TL;DR : I need something similar to redux but it needs to be fast enough to not cause performance issues on an infinite loop.

Create a context with the ref. Wrap your root with the provider, and use the hook to get access to the ref when you need to use/update it:
import { createContext, useRef, useContext } from 'react';
const defaultValue = /** default value **/;
const MyValueContext = createContext(defaultValue);
const MyValueContextProvider = ({ children }) => {
const myValueRef = useRef(defaultValue);
return (
<MyValueContext.Provider value={myValueRef}>
{children}
</MyValueContext.Provider>
);
};
const useMyValue = () => useContext(MyValueContext);
To use in components call the useMyValue hook. This would give you direct access to the ref, and since you don't update any state (just change the ref's current property) it won't cause re-renders:
const WebGLStarter = (props) => {
const myValue = useMyValue();
myValue.current = someCalculation();
...
};

Related

Context value not accessible inside inner function of functional component

I am attempting to build a component that takes in an arbitrarily large list of items and displays a chunk of them at a time. As the user scrolls the window down, I want to automatically load more items if any exist.
The problem I am running into is that the my appState variable is not acting consistently. When I log it at the top of the component, it always reads the correct value out of the loaded context. However, when I read the value inside the onScroll function, it always returns the default uninitialized state. Where did my context go on the inner function?
Here's a stripped down version that illustrates my problem:
Component
import { useContext } from 'react'
import { useLifecycles} from 'react-use'
import AppState from '../../models/AppState'
import { Context } from '../../store/create'
export default () => {
const appState:AppState = useContext(Context)
console.log('appState.items (root)=', appState.items.length) // Returns `100`, as it should
useLifecycles(
() => {
window.addEventListener('scroll', onScroll)
},
() => {
window.removeEventListener('scroll', onScroll)
}
)
const onScroll = (evt:any) => {
console.log('appState.items (onScroll)', appState.items.length) // Returns `0` (the default uninitialized state).
}
return (
<div className='ItemList'>
<h1>Hello world</h1>
{/* The list of items goes here */}
</div>
)
}
../../store/create
import React from 'react'
import AppState, { getDefaultState } from '../models/AppState'
let state:AppState = getDefaultState()
export const Context:React.Context<AppState> = React.createContext<AppState>(state)
export const setAppState = (newState:AppState):void => {
_state = newState
}
export const getAppState = ():AppState => {
return _state
}
I've read the rule of hooks, and to my understanding I am not breaking anything. My useContext and useLifecycle calls are in a fixed order at the top; no conditionals, no loops.
What am I missing?
I am not aware of how useLifecycles work. But the problem I can see is that you are binding the event a function. That function has the state in it's closure and so it captures that value of state. Whenever state changes, your handler isn't aware of the state change and so it just keeps using the data that was previously captured. Now to solve it, you need to listen for state change and remove the listener that was previously attached, add the new listener that has new values in its closure. I think the useLifecycles should have a dependency option to achieve that. If not the other way could be to use useEffect hook.
Edit:
I just checked the react-use docs and turns out what you really need is useEvent. Look at the example in docs. To make sure it works in your case, you should pass your dependency in useCallback.

Reactivity when element added to DOM

I want to integrate qr-scanner to my project. This library is capable of decoding QR-Codes by utilizing the camera. It just needs the reference to a html-video-element and then renders the webcam-stream and optionally some indicators of a QR-code being found to this element.
The easiest component would be something like this:
import { useRef } from "react";
import QrScanner from "qr-scanner";
export const QrComponent = () => {
const videoElement = useRef(null);
const scanner = new QrScanner(videoElement.current, (result) => {
console.log(result)
})
return (
<video ref={videoElement} />
)
}
however, qr-scanner checks, if the passed target-element is already part of the DOM:
if (!document.body.contains(video)) {
document.body.appendChild(video);
shouldHideVideo = true;
}
the video-element will never be added to the DOM, when the QrScanner-object is created. This leads to shouldHideVideo being set to true, which disables the video altogether later in the library-code.
So I think I need some kind of way to react to the video-element being added to the DOM. I thougt about using a MutationObserver (and tried it out by stealing the hook from this page), however I only wanted to print out all mutations using the hook like this:
import { useRef, useCallback } from "react";
import QrScanner from "qr-scanner";
import { useMutationObservable } from "./useMutationObservable";
export const QrComponent = () => {
const videoElement = useRef(null);
const scanner = new QrScanner(videoElement.current, (result) => {
console.log(result)
})
const onMutation = useCallback((mutations) => console.log(mutations), [])
useMutationObservable(document, onMutation)
return (
<video ref={videoElement} />
)
}
however, I never got a single line printed, so to me it seems, as if there are no mutations there.
Did I maybe misunderstand something? How can I react to the video-element being added to the document?
Ok, so I think I didn't provide enough information, but I found it out on my own:
The MutationObserver doesn't work, because I told it to watch document, yet I'm actually inside of a shadowdom with this particular component! So I suspect that mutations to the shadowdom won't be detected.
Also, because I'm inside of a shadowdom, document.contains(videoElem.current) will never be true. So in this particular case I have no better choice, than to copy the code of qr-scanner into my project-tree and adapt as needed.
On another note: useLayoutEffect is a react-hook, that is scheduled to run after DOM-mutations. So that's what I would use if I had to start over with this idea.

Children useCallback dependency hell

From what I understand you use useCallback to prevent rerendering so I've been using it in every function and my spider senses are telling me it already sounds bad.
But the story doesn't ends there, since I've been using it everywhere I'm now passing dependencies to all my child components that they shouldn't need to worry about like in the following example :
EDIT // SANDBOX: https://codesandbox.io/s/bold-noether-0wdnp?file=/src/App.js
Parent component (needs colorButtons and currentColor)
const ColorPicker = ({onChange}) => {
const [currentColor, setCurrentColor] = useState({r: 255, g:0, b: 0})
const [colorButtons, setColorButtons] = useState({0: null})
const handleColorButtons = useCallback((isToggled, id) => {
/* code that uses colorButtons and currentColor */
}, [colorButtons, currentColor])
return <div className="color-picker">
<RgbColorPicker color={currentColor} onChange={setCurrentColor} />
<div className="color-buttons">
{
Object.entries(colorButtons).map(button => <ColorButton
//...
currentColor={currentColor}
onClick={handleColorButtons}
colorButtons={colorButtons}
/>)
}
</div>
</div>
}
1st child (needs style and currentColor but gets colorButtons for free from its parent)
const ColorButton = ({currentColor, onClick, id, colorButtons}) => {
const [style, setStyle] = useState({})
const handleClick = useCallback((isToggled) => {
/* code that uses setStyle and currentColor */
}, [style, currentColor, colorButtons])
return <ToggleButton
//...
onClick={handleClick}
style={style}
dependency1={style}
dependency2={currentColor}
dependency3={colorButtons}
>
</ToggleButton>
}
2nd child (only needs its own variables but gets the whole package)
const ToggleButton = ({children, className, onClick, style, data, id, onRef, ...dependencies}) => {
const [isToggled, setIsToggled] = useState(false)
const [buttonStyle, setButtonStyle] = useState(style)
const handleClick = useCallback(() => {
/* code that uses isToggled, data, id and setButtonStyle */
}, [isToggled, data, id, ...Object.values(dependencies)])
return <button
className={className || "toggle-button"}
onClick={handleClick}
style={buttonStyle || {}}
ref={onRef}
>
{children}
</button>
}
Am I doing an anti-pattern and if so, what is it and how to fix it ? Thanks for helping !
React hook useCallback
useCallback is a hook that can be used in functional React components. A functional component is a function that returns a React component and that runs on every render, which means that everything defined in its body get new referential identities every time. An exception to this can be accomplished with React hooks which may be used inside functional components to interconnect different renders and maintain state. This means that if you save a reference to a regular function defined in a functional component using a ref, and then compare it to the same function in a later render, they will not be the same (the function changes referential identity between renderings):
// Render 1
...
const fnInBody = () => {}
const fn = useRef(null)
console.log(fn.current === fnInBody) // false since fn.current is null
fn.current = fnInBody
...
// Render 2
...
const fnInBody = () => {}
const fn = useRef(null)
console.log(fn.current === fnInBody) // false due to different identity
fn.current = fnInBody
...
As per the docs, useCallback returns "a memoized version of the callback that only changes if one of the dependencies has changed" which is useful "when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders".
To sum up, useCallback will return a function that maintains its referential identity (e.g. is memoized) as long as the dependencies don't change. The returned function contains a closure with the used dependencies and must thus be updated once the dependencies change.
This results in this updated version of the previous example
// Render 1
...
const fnInBody = useCallback(() => {}, [])
const fn = useRef(null)
console.log(fn.current === fnInBody) // false since fn.current is null
fn.current = fnInBody
...
// Render 2
...
const fnInBody = useCallback(() => {}, [])
const fn = useRef(null)
console.log(fn.current === fnInBody) // true
fn.current = fnInBody
...
Your use case
Keeping the above description in mind, let's have a look at your use of useCallback.
Case 1: ColorPicker
const handleColorButtons = useCallback((isToggled, id) => {
/* code that uses colorButtons and currentColor */
}, [colorButtons, currentColor])
This function will get a new identity every time colorButtons or currentColor changes. ColorPicker itself rerenders either when one of these two are set or when its prop onChange changes. Both handleColorButtons and the children should be updated when currentColor or colorButtons change. The only time the children benefit from the use of useCallback is when only onChange changes. Given that ColorButton is a lightweight component, and that ColorPicker rerenders mostly due to changes to currentColor and colorButtons, the use of useCallback here seems redundant.
Case 2: ColorButton
const handleClick = useCallback((isToggled) => {
/* code that uses setStyle and currentColor */
}, [style, currentColor, colorButtons])
This is a situation similar to the first case. ColorButton rerenders when currentColor, onClick, id or colorButtons change and the children rerender when handleClick, style, colorButtons or currentColor change. With useCallback in place, the props id and onClick may change without rerendering the children (according to the above visible code at least), all other rerenders of ColorButton will lead to its children rerendering. Again, the child ToggleButton is lightweight and id or onClick are not likely to change more often than any other prop so the use of useCallback seems redundant here as well.
Case 3: ToggleButton
const handleClick = useCallback(() => {
/* code that uses isToggled, data, id and setButtonStyle */
}, [isToggled, data, id, ...Object.values(dependencies)])
This case is elaborate with a lot of dependencies but from what I see, one way or the other, most of the component props will lead to a "new version" of handleClick and with the children being lightweight components, the argument to use useCallback seems weak.
So when should I use useCallback?
As the docs say, use it in the very specific cases when you need a function to have referential equality between renders ...
You have a component with a subset of children that are expensive to rerender and that should rerender much less often than the parent component but rerender due to a function prop changing identity whenever the parent rerenders. To me, this use case also signals bad design and I would attempt to divide the parent component into smaller components but what do I know, maybe this is not always possible.
You have a function in the body of the functional component which is used in another hook (listed as a dependency) which is triggered every time due to the function changing identity whenever the component rerenders. Typically, you can omit such a function from the dependency array by ignoring the lint rule even if this is not by the book. Other suggestions are to place such a function outside the body of the component or inside the hook that uses it, but there might be scenarios where none of this works out as intended.
Good to know connected to this is ...
A function living outside a functional component will always have referential equality between renders.
The setters returned by useState will always have referential equality between renders.
I said in the comments that you can use useCallback when there is function doing expensive calculations in a component that rerenders often but I was a bit off there. Let's say you have a function that does heavy calculations based on some prop that changes less often than a component rerenders. Then you COULD use useCallback and run a function inside it that returns a function with a closure with some computed value
const fn = useCallback(
(
() => {
const a = ... // heavy calculation based on prop c
const b = ... // heavy calculation based on prop c
return () => { console.log(a + b) }
}
)()
, [c])
...
/* fn is used for something, either as a prop OR for something else */
This would effectively avoid calculating a and b every time the component rerenders without c changing, but the more straightforward way to do this would be to instead
const a = useMemo(() => /* calculate and return a */, [c])
const b = useMemo(() => /* calculate and return b */, [c])
const fn = () => console.log(a + b)
so here the use of useCallback just complicates things in a bad way.
Conclusion
It's good to understand more complicated concepts in programming and to be able to use them, but part of the virtue is also to know when to use them. Adding code, and especially code that involves complicated concepts, comes at the price of reduced readability and code that is harder to debug with a lot of different mechanisms that interplay. Therefore, make sure you understand the hooks, but always try to not use them if you can. Especially useCallback, useMemo and React.memo (not a hook but a similar optimization) should, in my opinion, only be introduced when they are absolutely needed. useRef has its very own use cases but should also not be introduced when you can solve your problem without it.
Good work on the sandbox! Always makes it easier to reason about code. I took the liberty of forking your sandbox and refactoring it a bit: sandbox link. You can study the changes yourself if you want. Here is a summary:
It's good that you know and use useRef and useCallback but I was able to remove all the uses, making the code much easier to understand (not only by removing these uses but by also removing the contexts where they are used).
Try to work with React to simplify things. I know this is not a hands-on suggestion but the more you get into React, the more you will realize that you can do things co-operating with React, or you can do things your own way. Both will work but the latter will result in more headache for you and everybody else.
Try to isolate the scope of a component; only delegate data that is necessary to child components and constantly question where you keep your state. Earlier you had click handlers in all three components and the flow was so complicated I didn't even bother to fully understand it. In my version, there is just one click handler in ColorPicker that is being delegated down. The buttons don't have to know what happens when you click them as long as the click handler takes care of that. Closures and the ability to pass functions as arguments are strong advantages of React and Javascript.
Keys are important in React and it's good to see that you use them. Typically, the key should correspond to something that uniquely identifies a specific item. Good to use here would be ${r}_${g}_${b} but then we would only be able to have one sample of each color in the button array. This is a natural limitation but if we don't want it, the only way to assign keys is to assign a unique identifier, which you did. I prefer using Date.now() but some would probably advise against it for some reason. You could use a global variable outside the functional component too if you don't want to use a ref.
Try to do things the functional (immutable) way, and not the "old" Javascript way. For example, when adding to an array, use [...oldArray, newValue] and when assigning to an object, use {...oldObject, newKey: newValue }.
There are more things to say but I think it's better for you to study the refactored version and you can let me know if you wonder about anything.

How to give react components dynamic ids, when React runs code twice?

It's a known React behavior that code runs twice.
However, I'm creating a form builder in which I need to be able to give each form input a dynamic Id and use that Id for a lot of other purposes later. Here's a simple code of an input:
const Text = ({placeholder}) => {
const [id, setId] = useState(Math.random());
eventEmitter.on('global-event', () => {
var field = document.querySelector(`#${id}`); // here, id is changed
});
}
But since Math.random() is a side-effect, it's called twice and I can't create dynamic ids for my form fields.
The reason I'm using document.querySelector can be read here.
My question is, how can I create consistent dynamic ids for my inputs?
It seems you think that useState(Math.random()); is the side-effect causing you issue, but only functions passed to useState are double-invoked.
I think the issue you have is that the eventEmitter.on call is the unintentional side-effect since the function component body is also double invoked.
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies <-- this
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer <-- not this
To remedy this I believe you should place the eventEmitter.on logic into an useEffect hook with a dependency on the id state. You should also probably use id values that are guaranteed a lot more uniqueness. Don't forget to return a cleanup function from the effect to remove any active event "listeners", either when id updates, or when the component unmounts. This is to help clear out any resource leaks (memory, sockets, etc...).
Example:
import { v4 as uuidV4 } from 'uuid';
const Text = ({placeholder}) => {
const [id, setId] = useState(uuidV4());
useEffect(() => {
const handler = () => {
let field = document.querySelector(`#${id}`);
};
eventEmitter.on('global-event', handler);
return () => {
eventEmitter.removeListener('global-event', handler);
};
}, [id]);
...
}

How to implement your own useMemo from Scratch, for React

Basically this:
function MyComponent() {
let [count, setCount] = useState(1)
let memo = useMyMemo(() => new MyClass)
return <div onClick={update}>{count}</div>
function update() {
setCount(count + 1)
}
}
function useMyMemo(fn) {
// what to do here?
}
class MyClass {
}
I would like for useMyMemo to only return 1 instance of the class per component instance. How do I set this up to implement it without resorting to using any of the existing React hooks? If it's not possible without the React hooks, they why not? If it is possible only through accessing internal APIs, how would you do that?
As a bonus it would be helpful to know how it could be passed property dependencies, and how it would use that to figure out if the memo should be invalidated.
I think you're describing how useMemo already works, assuming you pass [] as the dependencies parameter. It should already create one instance of MyClass per instance of MyComponent. However, the documentation says that future versions of useMemo will not guarantee that the value is only called once, so you might want to try useRef.
const memo = useRef(null);
if (!memo.current) {
memo.current = new MyClass()
}
If you want it to create a new instance of MyClass when dependencies change, you'll have to either use useMemo (and accept the chance that the value might be invalidated occasionally), or do your own shallow comparison against the previous value. Something like this:
const useMyMemo = (create, dependencies) => {
const val = React.useRef(create());
const prevDependencies = React.useRef([]);
if (!shallowEquals(dependencies, prevDependencies.current)) {
val.current = create();
prevDependencies.current = [...dependencies];
}
return val;
};
I'll leave the implementation of shallowEquals to you, but I believe lodash has one.
Now you really asked for a function that doesn't use any React hooks, and mine uses useRef. If you don't want useRef, you can create your own simple memoization function that always returns the same pointer regardless of changes to .current.
Simple mock implementation of useMemo using useRef.
Note: You can shallowCompare or deepCompare to compute isChanged based on your need. Example if dependencies are objects.
/* File: useMemo.js */
import { useRef } from "react";
export function useMemo(callback, deps) {
const mPointer = useRef([deps]); // Memory pointer (Make sure to read useRef docs)
const isChanged = mPointer.current[0].some(
// If dependencies changed
(item, index) => item !== deps[index]
);
if (mPointer.current.length === 1 || isChanged) {
// If first time or changed, compute and store it
mPointer.current = [deps, callback()];
}
return mPointer.current[1];
}
See CodeSandbox demo.
Read: useRef returns a mutable ref object whose .current property is
initialized to the passed argument (initialValue). The returned object
will persist for the full lifetime of the component.
Pure javascript mock using closure - [ For educational purpose ONLY! ]
/* File: useMemo.js */
function memo() {
const stack = {};
return (callback, deps) => {
/* Creating hashkey with dependencies + function source without whitespaces */
const hash = deps.join("/") + "/" + callback.toString().replace(/\s+/g, "");
if (!stack[hash]) {
stack[hash] = callback();
}
return stack[hash];
};
};
export const useMemo = memo();
See CodeSandbox demo.
Idea explaination : With above code trying to create a unique hash by combining dependencies value & function source using toString,
removing spaces in string to make hash as unique as possible. Using this as a key
in the hashmap to store the data and retrieve when called.
The above code is buggy, for example callback result that are cached are not removed when component unmount's. Adding useEffect and return clean-up function should fix this during unmount. Then this solution becomes tightly coupled with react framework
Hope this helps. Feedback is welcomed to improve the answer!

Categories

Resources