Is it possible to use conventional Javascript classes in models (in a MVC paradigm) using React instead of using Redux or contexts & reducers that seem to undermine reusability? If so, how can we effectively 'subscribe' to actions that change data in these objects so that views (i.e. React components) are rendered effectively?
The subscription logic is on you to figure out yourself. React doesn't offer any mechanism that can be "borrowed" in other classes. But event-oriented programming in Javascript is, of course, nothing new. So an event-emitter like pattern would work. So here is one example (not tested, but it should give you the idea):
class MyClass {
data = {};
constructor(onTick) {
setInterval(() => {
const now = new Date();
this.data.time = now;
this.data.tillChristmas = (new Date('2021-12-25')) - now;
onTick && onTick(this);
}, 10 * 1e3);
}
}
const MyComp = () => {
const [data, setData] = useState({});
useEffect(() => {
new MyClass(myclass => {
setData(myclass.data);
});
}, []);
return <pre>
{JSON.stringify(data, true, 2)}
</pre>;
// of course, you could present the data in more interesting ways, too.
};
Related
I stumbled upon a very interesting question and I would like to know how to best solve this in React. Assume the following code:
const [qrText, setQrText] = useState("")
...
const generateQrCode = () => {
// set other state inside the "then"
QRCode.toDataUrl(qrText).then(...)
}
const handleChange = (e) => {
setQrText(e.target.value)
generateQrCode()
}
This code is unsafe, since state updates are asynchronously, and by the time generateQrCode runs, qrText could still have the old value.
I always tended to solve this problem using a useEffect with dependency array:
const [qrText, setQrText] = useState("")
...
const handleChange = (e) => {
setQrText(e.target.value)
}
useEffect(() => {
const generateQrCode = () => {
// set other state inside the "then"
QRCode.toDataUrl(qrText).then(...)
}
generateQrCode()
}, [qrText])
However, I recently watched a YouTube video from a React conference, where a senior engineer said that useEffect is only supposed to be used to synchronize data with external services or the DOM. Instead, people should update state in event handlers only.
So is this the right way then to handle this scenario?
const [qrText, setQrText] = useState("")
...
// this now takes the qrText as argument
const generateQrCode = (qrTextArg) => {
// set other state inside the "then"
QRCode.toDataUrl(qrTextArg).then(...)
}
const handleChange = (e) => {
const value = e.target.value
setQrText(value)
generateQrCode(value) // pass the event value, instead of relying on the "qrText" state
}
This would equal the "event based" approached, but feels a bit imperative and not "react"-ish.
So I wonder, what is the intended way to do this?
Thanks for your answers!
A test in our frankly very complicated React app is taking ~1000 times longer to run than expected, and I'm trying to pin down where/why.
I started by manually calling console.time('name of test') in the test file, and then manually dotting console.timeLog('name of test', 'did a thing') around the application in all the places that were called and seemed likely to cause slow downs.
I noticed a lot of these places were inside React hooks - they aren't slow themselves, but were helping me see how long it took for coffee to get there.
I decided I needed to write a monkey patch in a Jest setupFilesAfterEnv file for logging when React hooks callback are called, and what with (for this example, I'll use useEffect)
const React = require('react');
let timeTestName = null;
let doTimeLog = false;
let prevUseEffect;
beforeAll(() => {
({ timeTestName } = global);
const prevUseEffect = React.useEffect;
React.useEffect = (cb, deps) => {
if(doTimeLog && timeTestName && Array.isArray(deps) && !__filename.includes('node_modules')){
console.timeLog(timeTestName, `Use Effect called with ${JSON.stringify(deps, null, 2)}`): // log useEffect being called with timer
}
prevUseEffect(cb, deps);
}
});
beforeEach(() => {
const { testPath } = expect.getState();
if(testPath.endsWith(`${timeTestName}.test.js`)) {
doTimeLog = true;
console.time(timeTestName); // start timer
} else {
doTimerLog = false;
}
});
afterEach(() => {
doTimerLog = false;
console.log(testToTimeName); // end timer
});
afterAll(() => {
React.useEffect = prevUseEffect;
})
However what I really want as well is the variable names in the dependency list, which I cannot get (at least not without changing non-test code).
One thought I had was using a Jest transformer to make all the arrays into objects so I preserve the variable names as keys; something like:
module.exports = {
process(sourceText) {
return {
code: `convertSquareToCurly(sourceText)`,
};
},
};
module.exports = {
transform: {
'matchDepList':
'<rootDir>/deplistTransformer.js',
},
};
So during tests only my useEffects become:
useEffect(() => foo(a, b, c), { a, b, c})
(I will handle this new object in my monkey patch)
I'm reasonably confident I can make the above work.
Then I in my monkey patch I can call console.log(Object.entries(deps)); and prevUseEffect(cb, Object.values(deps));.
However if I'm concerned that calling Object.values will cause the hook's callback to always be called.
I haven't been able to try the transformer yet, but I don't want to waste time writing it if passing Object.values won't work in place of the untransformed dependency list.
Is there another way to monkey patch and get variable names, or would this work?
My React app uses setTimeout() and setInterval(). Inside them, I need to access the state value. As we know, closures are bound to their context once created, so using state values in setTimeout() / setInterval() won't use the newest value.
Let's keep things simple and say my component is defined as such:
import { useState, useEffect, useRef } from 'react';
const Foo = () => {
const [number, setNumber] = useState(0);
const numberRef = useRef(number);
// Is this common? Any pitfalls? Can it be done better?
numberRef.current = number;
useEffect(
() => setInterval(
() => {
if (numberRef.current % 2 === 0) {
console.log('Yay!');
}
},
1000
),
[]
);
return (
<>
<button type="button" onClick={() => setNumber(n => n + 1)}>
Add one
</button>
<div>Number: {number}</div>
</>
);
};
In total I came up with 3 ideas how to achieve this, is any of them a recognized pattern?
Assigning state value to ref on every render, just like above:
numberRef.current = number;
The benefit is very simplistic code.
Using useEffect() to register changes of number:
useEffect(
() => numberRef.current = number,
[number]
);
This one looks more React-ish, but is it really necessary? Doesn't it actually downgrade the performance when a simple assignment from point #1 could be used?
Using custom setter:
const [number, setNumberState] = useState(0);
const numberRef = useRef(number);
const setNumber = value => {
setNumberState(value);
numberRef.current = value;
};
Is having the same value in the state and the ref a common pattern with React? And is any of these 3 ways more popular than others for any reason? What are the alternatives?
2021-10-17 EDIT:
Since this looks like a common scenario I wanted to wrap this whole logic into an intuitive
useInterval(
() => console.log(`latest number value is: ${number}`),
1000
)
where useInterval parameter can always "access" latest state.
After playing around for a bit in a CodeSandbox I've come to the realization that there is no way someone else hasn't already thought about a solution for this.
Lo and behold, the man himself, Dan Abramov has a blog post with a precise solution for our question https://overreacted.io/making-setinterval-declarative-with-react-hooks/
I highly recommend reading the full blog since it describes a general issue with the mismatch between declarative React programming and imperative APIs. Dan also explains his process (step by step) of developing a full solution with an ability to change interval delay when needed.
Here (CodeSandbox) you can test it in your particular case.
ORIGINAL answer:
1.
numberRef.current = number;
I would avoid this since we generally want to do state/ref updates in the useEffect instead of the render method.
In this particular case, it doesn't have much impact, however, if you were to add another state and modify it -> a render cycle would be triggered -> this code would also run and assign a value for no reason (number value wouldn't change).
2.
useEffect(
() => numberRef.current = number,
[number]
);
IMHO, this is the best way out of all the 3 ways you provided. This is a clean/declarative way of "syncing" managed state to the mutable ref object.
3.
const [number, setNumberState] = useState(0);
const numberRef = useRef(number);
const setNumber = value => {
setNumberState(value);
numberRef.current = value;
};
In my opinion, this is not ideal. Other developers are used to React API and might not see your custom setter and instead use a default setNumberState when adding more logic expecting it to be used as a "source of truth" -> setInterval will not get the latest data.
You have simply forgotten to clear interval. You have to clear the interval on rendering.
useEffect(() => {
const id = setInterval(() => {
if (numberRef.current % 2 === 0) {
console.log("Yay!");
}
}, 1000);
return () => clearInterval(id);
}, []);
If you won't clear, this will keep creating a new setInterval with every click. That can lead to unwanted behaviour.
Simplified code:
const Foo = () => {
const [number, setNumber] = useState(0);
useEffect(() => {
const id = setInterval(() => {
if (number % 2 === 0) {
console.log("Yay!");
}
}, 1000);
return () => clearInterval(id);
}, [number]);
return (
<div>
<button type="button" onClick={() => setNumber(number + 1)}>
Add one
</button>
<div>Number: {number}</div>
</div>
);
};
There's a commonly used utility hook "useLatest", which returns a ref containing the latest value of the input. There are 2 common implementations:
const useLatest = <T>(value: T): { readonly current: T } => {
const ref = useRef(value);
ref.current = value;
return ref;
};
From https://github.com/streamich/react-use/blob/master/src/useLatest.ts
const useLatest = <T extends any>(current: T) => {
const storedValue = React.useRef(current)
React.useEffect(() => {
storedValue.current = current
})
return storedValue
}
From https://github.com/jaredLunde/react-hook/blob/master/packages/latest/src/index.tsx
The first version isn't suitable for React 18's concurrent mode, the second version will return the old value if used before useEffect runs (e.g. during render).
Is there a way to implement this that's both concurrent-safe and consistently returns the correct value?
Here's my attempt:
function useLatest<T>(val: T): React.MutableRefObject<T> {
const ref = useRef({
tempVal: val,
committedVal: val,
updateCount: 0,
});
ref.current.tempVal = val;
const startingUpdateCount = ref.current.updateCount;
useLayoutEffect(() => {
ref.current.committedVal = ref.current.tempVal;
ref.current.updateCount++;
});
return {
get current() {
// tempVal is from new render, committedVal is from old render.
return ref.current.updateCount === startingUpdateCount
? ref.current.tempVal
: ref.current.committedVal;
},
set current(newVal: T) {
ref.current.tempVal = newVal;
},
};
}
This hasn't been thoroughly tested, just wrote it while writing this question, but it seems to work most of the time. It should be better than both versions above, but it has 2 issues: it returns a different object every time and it's still possible to be inconsistent in this scenario:
Render 1:
ref1 = useLatest(val1)
Create function1, which references ref1
Commit (useLayoutEffect runs)
Render 2:
useLatest(val2)
Call function1
function1 will use val1, but it should use val2.
Here is what I think is correct:
const useLatest = <T extends any>(current: T) => {
const storedValue = React.useRef(current)
React.useLayoutEffect(() => {
storedValue.current = current
})
return storedValue.current
}
Is there a way to implement this that's both concurrent-safe and consistently returns the correct value?
The question doesn't actually explain what "this" means, i.e. how is useLatest called, and what purpose it fulfills in the application. So I'll have to guess for that ;) A somewhat realistic example would be very helpful.
In any case, it's probably useful to take a step back and ask if useLatest is the most suitable solution. If you find you don't need it, you also won't have to fix it.
With the way it works (depending on an effect to capture the value), it indeed won't play well with concurrent features. But even without them, it's an unreliable approach as the ref theoretically can change at any point, making renders unpredictable.
My guess of the use case is something similar to the proposed (and partially accepted) useEvent hook (GitHub PR).
function Chat() {
const [text, setText] = useState('');
const onClick = useEvent(() => {
sendMessage(text);
});
return <SendButton onClick={onClick} />;
}
Its purpose is to capture the latest render's scope, like useCallback, but without the need for dependencies. It does this by using an unchanging callback that internally calls the latest created callback, in a ref that is re-assigned on every render. That way passing that callback as a prop won't cause any renders by itself.
You can implement this yourself already, but the RFC mentions some open questions about this approach.
export function useEvent(handler) {
const latestHandlerRef = useRef();
useLayoutEffect(() => {
latestHandlerRef.current = handler;
});
// Never changing callback.
return useCallback((...args) => {
latestHandlerRef.current(...args)
}, []);
}
I also tested setting latestHandlerRef.current = handler directly in render instead of the layout effect. For now this seems to work as expected but that's just my use case. In the PR some doubt is expressed over assigning to a ref during render, though possibly these concerns don't really apply here, as the ref is only ever accessed in the callback.
As an example of what I'd like to do, in Draft.js I'd like to allow the users to include a slideshow of images in the page that could change externally later on. So if there is a slideshow defined in my app, the user could select that slideshow to put in their page, and if the images were changed later then they would automatically update on the page. A visual representation in the editor would a nice-to-have as well. Am I asking too much or is this possible in Draft.js?
It never fails. I post a question and almost immediately I find something that might answer my question. Draft.js has what is called "decorators", which are currently documented here: https://draftjs.org/docs/advanced-topics-decorators.html
Basically you'd create a series of decorators using functions / components. Strategy takes a function, component is obvious.
const compositeDecorator = new CompositeDecorator([
{
strategy: handleStrategy,
component: HandleSpan,
},
{
strategy: hashtagStrategy,
component: HashtagSpan,
},
]);
Strategies can be defined using regexes. This enables you to write your own syntax or use that of a template engine's for embedding widgets. The strategies in the documentation are a fairly good example:
// Note: these aren't very good regexes, don't use them!
const HANDLE_REGEX = /\#[\w]+/g;
const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g;
function handleStrategy(contentBlock, callback, contentState) {
findWithRegex(HANDLE_REGEX, contentBlock, callback);
}
function hashtagStrategy(contentBlock, callback, contentState) {
findWithRegex(HASHTAG_REGEX, contentBlock, callback);
}
function findWithRegex(regex, contentBlock, callback) {
const text = contentBlock.getText();
let matchArr, start;
while ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
callback(start, start + matchArr[0].length);
}
}
And then here are the components:
const HandleSpan = (props) => {
return <span {...props} style={styles.handle}>{props.children}</span>;
};
const HashtagSpan = (props) => {
return <span {...props} style={styles.hashtag}>{props.children}</span>;
};