How exactly does useEffect's return work? Why is the code preforming like it does? - settimeout

I'm practicing react hooks and I'm creating a very simple stopwatch app. Currently, my code is doing exactly what I want it to do but I do not understand why it works. When I hit start, the setTimeouts run and constantly update the time state. When I hit stop, it clears the timeout. Why does it clear the timeout when I do not explicitly tell it to. Also, based on the react docs, the return in useEffect will only run when the component unmounts. However, I threw console.logs inside and saw that it runs the returned callback every time useEffect is called. Finally, I removed the returned callback and saw that it doesn't actually clear the timeout when I hit stop. Can someone help me dissect this?
import React, {useState, useEffect} from 'react';
function Stopwatch(){
const [time, setTime] = useState(0);
const [start, setStart] = useState(false);
useEffect(() => {
let timeout;
if (start) {
timeout = setTimeout(() => {setTime(currTime => currTime + 1);}, 1000);
}
return () => {
clearTimeout(timeout);
}
});
return(
<>
<div>{time}</div>
<button onClick={() => setStart(currStart => !currStart)}>{start ? "Stop" : "Start"}</button>
</>
)
}
export default Stopwatch

Why does it clear the timeout when I do not explicitly tell it to?
In your implementation useEffect runs after every re-render because you didn't specify the dependencies array, so if you start the timer and then in the middle press stop the clean up function is going to run and the last timeout will be cleared
It goes like this,
The component mounts -> useEffect callback fires and returns a function -> when the component re-renders, the returned function is executed and the cycle goes back to running the useEffect callback.
What you probably read in the docs had an empty dependencies array which is the second argument of useEffect
useEffect(() => {
console.log('will only run when the component mounts for the first time')
return () => {
console.log('will only run when the component unmounts')
}
}, []) // nothing inside the dependencies array, run this once
A better implementation of your component will be like this
function Stopwatch(){
const [time, setTime] = useState(0)
const [start, setStart] = useState(false)
useEffect(() => {
// when start is false there is no reason to set up a timer or return a
// cleanup function so lets just exit early
if (!start) return
// start is true, set up the interval
const intervalId = setInterval(() => setTime(prevTime => prevTime + 1), 1000)
// return a cleanup function that will run only when start changes
// to false
return () => clearInterval(intervalId)
}, [start]) // run this effect only when start changes
const toggleStart = () => setStart(prevStart => !prevStart)
return(
<>
<div>{time}</div>
<button onClick={toggleStart}>{start ? "Stop" : "Start"}</button>
</>
)
}

Related

How to test the component refresh behavior with react-testing-library?

I'm writing a test for a component that takes a few props such as "isLoading", "clients" and "refreshClients". refreshClients is defined like this:
function refreshClients() {
setIsLoading(true)
getClients().then(response => {
setClients(response)
setIsLoading(false)
})
}
clients and isLoading are states from the parent component that are used as props of the child component. The client component also gets the refreshClients prop, which allows it to update its own props (isLoading and clients), through the function passed in by the parent component.
This is one of the use cases: after taking an action over a client, like deletion, the component will call refreshClients, which should take care of reloading the clients table displayed in the component. While the new listing is being loaded the table isn't displayed (isLoading is true). The component works well, however I'm unsure on how to properly test it using testing-library. I'm basically calling rerender in the tests but I feel there should be a way to replicate this behavior in the tests...
Is there a way to create states to pass as props to the tested component? Or is there another recommended approach to handle cases like this?
In case it makes it easier to visualize the idea, here is a complete simple example of how it would work:
import {createRoot} from 'react-dom'
import React, {useState} from 'react';
export function App() {
const [isLoading, setIsLoading] = useState(false);
const [clients, setClients] = useState(['david', 'sara']);
const refreshClients = () => {
setIsLoading(true)
setTimeout(() => {
setClients(['john', 'mary'])
setIsLoading(false)
}, 1000)
}
return <ClientsTable isLoading={isLoading} clients={clients} refreshClients={refreshClients} />
}
function ClientsTable({ isLoading, clients, refreshClients}) {
const deleteClient = () => {
console.log('TODO: delete client')
refreshClients()
}
return (
<div>
{isLoading && <p>Loading... please wait</p>}
{!isLoading && clients.map(client => (
<div>{client} <button onClick={deleteClient}>delete</button></div>
))}
</div>
);
}
createRoot(
document.getElementById('root')
).render(<App />)

NEXTJS fix window is not defined on import [duplicate]

In my Next.js app I can't seem to access window:
Unhandled Rejection (ReferenceError): window is not defined
componentWillMount() {
console.log('window.innerHeight', window.innerHeight);
}
̶A̶n̶o̶t̶h̶e̶r̶ ̶s̶o̶l̶u̶t̶i̶o̶n̶ ̶i̶s̶ ̶b̶y̶ ̶u̶s̶i̶n̶g̶ ̶p̶r̶o̶c̶e̶s̶s̶.̶b̶r̶o̶w̶s̶e̶r ̶ ̶t̶o̶ ̶j̶u̶s̶t̶ ̶e̶x̶e̶c̶u̶t̶e̶ ̶ ̶y̶o̶u̶r̶ ̶c̶o̶m̶m̶a̶n̶d̶ ̶d̶u̶r̶i̶n̶g̶ ̶r̶e̶n̶d̶e̶r̶i̶n̶g̶ ̶o̶n̶ ̶t̶h̶e̶ ̶c̶l̶i̶e̶n̶t̶ ̶s̶i̶d̶e̶ ̶o̶n̶l̶y̶.
But process object has been deprecated in Webpack5 and also NextJS, because it is a NodeJS variable for backend side only.
So we have to use back window object from the browser.
if (typeof window !== "undefined") {
// Client-side-only code
}
Other solution is by using react hook to replace componentDidMount:
useEffect(() => {
// Client-side-only code
})
Move the code from componentWillMount() to componentDidMount():
componentDidMount() {
console.log('window.innerHeight', window.innerHeight);
}
In Next.js, componentDidMount() is executed only on the client where window and other browser specific APIs will be available. From the Next.js wiki:
Next.js is universal, which means it executes code first server-side,
then client-side. The window object is only present client-side, so if
you absolutely need to have access to it in some React component, you
should put that code in componentDidMount. This lifecycle method will
only be executed on the client. You may also want to check if there
isn't some alternative universal library which may suit your needs.
Along the same lines, componentWillMount() will be deprecated in v17 of React, so it effectively will be potentially unsafe to use in the very near future.
If you use React Hooks you can move the code into the Effect Hook:
import * as React from "react";
export const MyComp = () => {
React.useEffect(() => {
// window is accessible here.
console.log("window.innerHeight", window.innerHeight);
}, []);
return (<div></div>)
}
The code inside useEffect is only executed on the client (in the browser), thus it has access to window.
With No SSR
https://nextjs.org/docs/advanced-features/dynamic-import#with-no-ssr
import dynamic from 'next/dynamic'
const DynamicComponentWithNoSSR = dynamic(
() => import('../components/hello3'),
{ ssr: false }
)
function Home() {
return (
<div>
<Header />
<DynamicComponentWithNoSSR />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home
The error occurs because window is not yet available, while component is still mounting. You can access window object after component is mounted.
You can create a very useful hook for getting dynamic window.innerHeight or window.innerWidth
const useDeviceSize = () => {
const [width, setWidth] = useState(0)
const [height, setHeight] = useState(0)
const handleWindowResize = () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
}
useEffect(() => {
// component is mounted and window is available
handleWindowResize();
window.addEventListener('resize', handleWindowResize);
// unsubscribe from the event on component unmount
return () => window.removeEventListener('resize', handleWindowResize);
}, []);
return [width, height]
}
export default useDeviceSize
Use case:
const [width, height] = useDeviceSize();
componentWillMount() lifecycle hook works both on server as well as client side. In your case server would not know about window or document during page serving, the suggestion is to move the code to either
Solution 1:
componentDidMount()
Or, Solution 2
In case it is something that you only want to perform in then you could write something like:
componentWillMount() {
if (typeof window !== 'undefined') {
console.log('window.innerHeight', window.innerHeight);
}
}
In the constructor of your class Component you can add
if (typeof window === 'undefined') {
global.window = {}
}
Example:
import React, { Component } from 'react'
class MyClassName extends Component {
constructor(props){
super(props)
...
if (typeof window === 'undefined') {
global.window = {}
}
}
This will avoid the error (in my case, the error would occur after I would click reload of the page).
global?.window && window.innerHeight
It's important to use the operator ?., otherwise the build command might crash.
Best solution ever
import dynamic from 'next/dynamic';
const Chart = dynamic(()=> import('react-apexcharts'), {
ssr:false,
})
A bit late but you could also consider using Dynamic Imports from next turn off SSR for that component.
You can warp the import for your component inside a dynamic function and then, use the returned value as the actual component.
import dynamic from 'next/dynamic'
const BoardDynamic = dynamic(() => import('../components/Board.tsx'), {
ssr: false,
})
<>
<BoardDynamic />
</>
I have to access the hash from the URL so I come up with this
const hash = global.window && window.location.hash;
Here's an easy-to-use workaround that I did.
const runOnClient = (func: () => any) => {
if (typeof window !== "undefined") {
if (window.document.readyState == "loading") {
window.addEventListener("load", func);
} else {
func();
}
}
};
Usage:
runOnClient(() => {
// access window as you like
})
// or async
runOnClient(async () => {
// remember to catch errors that might be raised in promises, and use the `await` keyword wherever needed
})
This is better than just typeof window !== "undefined", because if you just check that the window is not undefined, it won't work if your page was redirected to, it just works once while loading. But this workaround works even if the page was redirected to, not just once while loading.
I was facing the same problem when i was developing a web application in next.js This fixed my problem, you have to refer to refer the window object in a life cycle method or a react Hook. For example lets say i want to create a store variable with redux and in this store i want to use a windows object i can do it as follows:
let store
useEffect(()=>{
store = createStore(rootReducers, window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__())
}, [])
....
So basically, when you are working with window's object always use a hook to play around or componentDidMount() life cycle method
I wrapped the general solution (if (typeof window === 'undefined') return;) in a custom hook, that I am very pleased with. It has a similiar interface to reacts useMemo hook which I really like.
import { useEffect, useMemo, useState } from "react";
const InitialState = Symbol("initial");
/**
*
* #param clientFactory Factory function similiar to `useMemo`. However, this function is only ever called on the client and will transform any returned promises into their resolved values.
* #param deps Factory function dependencies, just like in `useMemo`.
* #param serverFactory Factory function that may be called server side. Unlike the `clientFactory` function a resulting `Promise` will not be resolved, and will continue to be returned while the `clientFactory` is pending.
*/
export function useClientSideMemo<T = any, K = T>(
clientFactory: () => T | Promise<T>,
deps: Parameters<typeof useMemo>["1"],
serverFactory?: () => K
) {
const [memoized, setMemoized] = useState<T | typeof InitialState>(
InitialState
);
useEffect(() => {
(async () => {
setMemoized(await clientFactory());
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return typeof window === "undefined" || memoized === InitialState
? serverFactory?.()
: memoized;
}
Usage Example:
I am using it to dynamically import libaries that are not compatible with SSR in next.js, since its own dynamic import is only compatible with components.
const renderer = useClientSideMemo(
async () =>
(await import("#/components/table/renderers/HighlightTextRenderer"))
.HighlightTextRendererAlias,
[],
() => "text"
);
As you can see I even implemented a fallback factory callback, so you may provide a result when initially rendering on the server aswell. In all other aspects this hook should behave similiar to reacts useMemo hook. Open to feedback.
For such cases, Next.js has Dynamic Import.
A module that includes a library that only works in the browser, it's suggested to use Dynamic Import. Refer
Date: 06/08/2021
Check if the window object exists or not and then follow the code along with it.
function getSelectedAddress() {
if (typeof window === 'undefined') return;
// Some other logic
}
For Next.js version 12.1.0, I find that we can use process.title to determine whether we are in browser or in node side. Hope it helps!
export default function Projects(props) {
console.log({ 'process?.title': process?.title });
return (
<div></div>
);
}
1. From the terminal, I receive { 'process?.title': 'node' }
2. From Chrome devtool, I revice { 'process?.title': 'browser' }
I had this same issue when refreshing the page (caused by an import that didn't work well with SSR).
What fixed it for me was going to pages where this was occurring and forcing the import to be dynamic:
import dynamic from 'next/dynamic';
const SomeComponent = dynamic(()=>{return import('../Components/SomeComponent')}, {ssr: false});
//import SomeComponent from '../Components/SomeComponent'
Commenting out the original import and importing the component dynamically forces the client-side rendering of the component.
The dynamic import is covered in Nextjs's documentation here:
https://nextjs.org/docs/advanced-features/dynamic-import
I got to this solution by watching the youtube video here:
https://www.youtube.com/watch?v=DA0ie1RPP6g
You can define a state var and use the window event handle to handle changes like so.
const [height, setHeight] = useState();
useEffect(() => {
if (!height) setHeight(window.innerHeight - 140);
window.addEventListener("resize", () => {
setHeight(window.innerHeight - 140);
});
}, []);
You can try the below code snippet for use-cases such as - to get current pathname (CurrentUrl Path)
import { useRouter } from "next/router";
const navigator = useRouter()
console.log(navigator.pathname);
For anyone who somehow cannot use hook (for example, function component):
Use setTimeout(() => yourFunctionWithWindow()); will allow it get the window instance. Guess it just need a little more time to load.
I want to leave this approach that I found interesting for future researchers. It's using a custom hook useEventListener that can be used in so many others needs.
Note that you will need to apply a little change in the originally posted one, like I suggest here.
So it will finish like this:
import { useRef, useEffect } from 'react'
export const useEventListener = (eventName, handler, element) => {
const savedHandler = useRef()
useEffect(() => {
savedHandler.current = handler
}, [handler])
useEffect(() => {
element = !element ? window : element
const isSupported = element && element.addEventListener
if (!isSupported) return
const eventListener = (event) => savedHandler.current(event)
element.addEventListener(eventName, eventListener)
return () => {
element.removeEventListener(eventName, eventListener)
}
}, [eventName, element])
}
If it is NextJS app and inside _document.js, use below:
<script dangerouslySetInnerHTML={{
__html: `
var innerHeight = window.innerHeight;
`
}} />

Why does Solid.js createEffect not re-run when a signal is in a setTimeout callback?

In Solid, why does this effect not re-run when count is updated? After some tinkering, I've found that it has to with count being in the setTimeout callback function, but what's the intuitive way to understand what things inside an effect are tracked and what things aren't?
function Counter() {
const [count, setCount] = createSignal(0);
createEffect(() => {
setTimeout(() => {
setCount(count() + 1);
}, 1000);
})
return (
<>
{count()}
</>
);
}
You can think about it this way (this is pretty much how the source code works):
let Listener
function Counter() {
const [count, setCount] = createSignal(0);
createEffect(() => {
Listener = thisEffect
setTimeout(() => {
setCount(count() + 1);
}, 1000);
Listener = null
})
return (
<>
{count()}
</>
);
}
As you can see the effect will set itself as the listener (tracking context) when the function starts and then will reset the listener (to the previous listener if it exists, in this case it doesn't).
So the effect will be the tracking context only during the execution of the callback you provided to createEffect as the argument. setTimeout delays the execution of whatever you put in it, so once the callback you put in setTimeout executes, the effect callback will have already finished executing, which means that it has already reset the listener, so the effect is not listening to signals anymore.
That is because the effect can not re-subscribe to the signal once it is run. Here is why:
Solid runs synchronously. Every signal keeps its own subscribers list. Effects are added to the subscribers list when they read the signal and removed when they are called back. So, subscribers list is renewed in each update cycle and it happens synchronously.
However setTimeout's callback is run asynchronously in the event loop.
When the callback runs, it will update the signal's value and the effect wrapping the setTimeout function will be added to the subscribers list. However this subscribers list gets discarded when the signal completes its execution cycle. So, the effect will never be called back. In other words, the effect will be subscribing to the subscribers list of the previous execution cycle.
The problem is not that the callback is unable to update the signal, (actually it does, that is why counter increments by one), but the effect is unable to re-subscribe to the signal's queue. So, we need to find a way to make the effect re-subscribe to the signal.
You have two options which produce different outputs:
Reads the signal synchronously which makes the effect re-subscribe and set a new timer whenever signal updates:
import { render } from "solid-js/web";
import { createSignal, createEffect } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
createEffect(() => {
const c = count();
setTimeout(() => {
setCount(c + 1);
}, 1000);
})
return (
<>
{count()}
</>
);
}
We read the signal's value in advance, long before the setTimeout's callback gets fired.
Get the right owner and subscribe to its list:
function Counter() {
const [count, setCount] = createSignal(0);
const owner = getOwner();
setTimeout(() => {
runWithOwner(owner!, () => {
createEffect(() => {
console.log('Running Effect');
setCount(count() + 1);
});
});
}, 1000);
return (
<>
{count()}
</>
);
}
In this solution, we create the effect when the setTimeout's callback is fired and bind the effect to the current owner.
An important side note: Your code will cause an infinite loop because you are setting the signal inside the effect, which runs whenever signal updates.
createEffect(() => {
setCount(count() + 1);
});
You can read more about runWithOwner function here: https://www.solidjs.com/docs/latest/api#runwithowner

clearInterval and eventListener

My problem is, I'm not being able to use the 'setInterval' attached to a button and use another one to stop it with 'clearInterval'.
My thought process:
Since clearInterval() would need a target, I stored the setInterval() inside a variable. However, I noticed that setInterval() starts running without being triggered by the eventListener attached to it. I was expecting this behaviour of firing on its own only if I had put setInterval() out in the open without being stored on a variable and on the global scope.
So then I tried to encapsulate it inside a function, only this time the clearInterval() seemed to 'lose' the target.
I had a look on some situtations and couldn't find an explanation that would satisfy my case. Right now it looks like I can't have both ways (a btn to 'fire' the timer and a btn to shut it off).
I don't know if this is a matter of scope,event handling or targeting. Had a look on HTMLStandard but couldn't make much use of it.
Thank you
<script>
let repeater = function () {
setInterval(() => {
console.log('loading');
}, 1000);
};
const startButton = document.querySelector('#firstB');
const stopButton = document.querySelector('#secondB');
startButton.addEventListener('click', repeater);
stopButton.addEventListener('click', () => {
clearInterval(repeater);
console.log('finished');
});
</script>
const startButton = document.querySelector("#firstB");
const stopButton = document.querySelector("#secondB");
let repeater;
startButton.addEventListener("click", function () {
repeater = setInterval(() => {
console.log("loading");
}, 1000);
});
stopButton.addEventListener("click", () => {
clearInterval(repeater);
console.log("finished");
});
This should work.

res.data won't save to state correctly

I have a MERN app which pulls data from a collection in MongoDB to render a timer component in the DOM. Currently in my collection, I have three timers titled first timer, another timer and even another. When I make a get request and run console.log(res.data), I see all the timers and their relevant data logged to the console. However, when I try to set state of timers using the useState hook, only the last timer is saved to state. Here is the code of my component:
function Wrapper() {
const [timers, setTimers] = useState([]);
const [title, setTitle] = useState('');
useEffect(() => {
axios
.get('http://localhost:3001/')
.then((res) => {
console.log(res.data);
res.data.map((timer) => {
let newTimer = (
<Timer title={timer.title} id={timer._id} time={timer.time} />
);
let allTimers = timers.slice();
allTimers.push(newTimer);
setTimers(allTimers);
console.log(allTimers);
});
})
.catch((err) => {
console.log(err);
});
}, []);
Here I am making my get request and mapping through the res.data to create a new timer component for each iteration. Then, I make a copy of timers (since state is immutable), push my new timer to allTimers variable and finally run setTimers(allTimers). Here is what React renders:
I expect allTimers to contain one, two and then all three of the timers in my database when logged to the consol on line 17. However, only the most recent timer shows up so it seems like I am setting state incorrectly but I'm not sure how. Anyone have any suggestions?
Because you set useEffect to Only runs on initial render, it always refers to the initial state of timers, which is an empty array, and even as you try to update it with useState but on the next loop it still refers to the empty array. The only update to have real effect is the last one, pushing the third timer into the empty array.
You can move your entire map loop to the return statement of the functional component to render an element for each timer.