I've got stuck with a problem of hook reusage when state and redux store should work together. I've simplified a code to show the problem.
There is a component where I want to use multiple hooks (for simplicity I re-use useMouseDown hook here):
export function Counter() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const plusSighRef = useRef<HTMLButtonElement>(null);
useMouseDown({
ref: plusSighRef,
onMouseDown: () => {
console.log('in first hook');
dispatch(increment());
}
});
useMouseDown({
ref: plusSighRef,
onMouseDown: () => { console.log('in second hook'); }
});
return <button ref={plusSighRef}>+</button>;
}
Each hook has inner state and has own callback on mouse down event:
const useMouseDown = ({ ref, onMouseDown }) => {
const [isClicked, setIsClicked] = useState(false);
useEffect(() => {
const element = ref.current;
const down = (e: MouseEvent) => {
setIsClicked(true);
onMouseDown(e);
}
element.addEventListener('mousedown', down);
return (): void => {
element.removeEventListener('mousedown', down);
};
}, [onMouseDown, ref]);
}
As a result a mousedown event in second hook is never triggered. The problem is that re-render occurs earlier than second hook is started.
I found some solutions but don't like both:
use something like setTimeout(() => dispatch(increment()), 0) inside the first hook mousedown prop. But it seems to be not obvious in terms of re-usage.
rewrite two hooks into one and manipulate with one "big" mousedown handler. But in that case a combined hook could be difficult for maintaining.
So I need a solution that allow to retain structure as is (I mean two separate hooks), but has second hook is working too. Could someone help how to get it?
Can't you just keep your logic in two different functions and execute it sequentially in the hook's onMouseDown function?
const executeFirstFlow = () => {
console.log('in first function');
dispatch(increment());
};
const executeSecondFlow = () => {
console.log('in second function');
};
useMouseDown({
ref: plusSighRef,
onMouseDown: () => {
executeFirstFlow();
executeSecondFlow();
}
});
Related
useEvent solves the problem of reading latest props/state in a callback inside useEffect, but can't be used in production yet [Nov 22].
It's use case is also documented in beta docs as well
The problem
const SomeComponent = ({ prop1, ...}) => {
const [state, setState] = useState('initial')
useEffect(() => {
// inside the connect callback I want to refer to latest state and props and I dont want to reconnect on state change
// here event handler, depends on prop1 and state
const connection = createConnection(...)
connection.on('connect', () => {
// will refer to prop, state
// without these varaibles in depedency array
// this effect will not see the latest values
})
return () => {
connection.disconnect()
}
}, [])
useEffect depends on depends on prop1 and state, causing unnecessary reconnections.
Some patch work like solution using useRef
const SomeComponent = ({ prop1, ...}) => {
const [state, setState] = useState()
const someFunction = () => {
// use latest props1
// use latest state
}
const someFunctionRef = useRef()
someFunctionRef.current = someFunction;
useEffect(() => {
const someFunctionRefWrapper = () => {
someFunctionRef.current()
}
// need to read the latest props/state variables
// but not rerender when those values change
const connection = createConnection(...)
connection.on('connect', someFunctionRefWrapper)
return () => {
connection.disconnect()
}
}, [])
Right now useEvent can't be used in production, I am thinking of creating a custom hook to solve the problem
const usePoorMansUseEvent(callback) {
const itemRef = useRef();
itemRef.current = callback;
const stableReference = useCallback((..args) => {
itemRef.current(...args)
}, []);
return stableReference;
}
Is there any better approach, am I reinventing the wheel
You should be able to just make two useEffects, one for connecting / disconnecting and one for refreshing the callback.
If you just want a connection on mount:
const [connection] = useState(createConnection(...));
useEffect(() => {
return () => {
connection.disconnect()
}
}, [connection])
useEffect(() => {
connection.on('connect', someCallback)
return () => {
// Disconnect previous callback on change, idk the actual syntax
connection.off('connect', someCallback)
}
}, [connection, someCallback]
That follows the whole immutability principle.
I want to understand the utility of useCallback in ReactJs. I read that useCallback is used to memoise the function inside it, and to trigger the callback depending by dependecies. How i notice we should use this hook when pass a function as a prop. In the same time i found an example on the internet and i can't figure out why the hook is used.
const useAsync = () => {
const [data, setData] = useState(null)
const execute = useCallback(() => {
setLoading(true)
return asyncFunc()
.then(res => {
setData(res)
return res
})
}, [])
}
Why execute function is wrapped by this hook in this example? And in general should we use useCallback if we don't pass a function as a parameter in a compoenent?
Definition:
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).
So yes it returns a memoized callback, but is basically used, in general, to factorize some redundant operations (like a call to an API).
In your case, suppose you have a useCallback like this:
const useAsync = (asyncFunc) => {
const [data, setData] = useState(null)
const execute = useCallback(() => {
return asyncFunc()
.then(res => {
setData(res)
return res
})
}, [asyncFunc])
return { execute, data };
}
Now let's use it in a component:
import React, { useEffect } from 'react';
function App() {
const { execute, data } = useAsync(myFunction);
useEffect(() => {
execute();
}, [execute]);
return (
<div>
{data.map(el => ...)}
</div>
);
}
Where myFunction is:
function myFunction() {
return fetch('http://localhost:3001/users/')
.then((response) => {
return response.json().then((data) => {
return data;
}).catch((err) => {
console.log(err);
})
});
}
Well, the result is that, data now are filled with the response coming from 'http://localhost:3001/users/' route.
Ok so now you could say "Yes but what's the difference between this verbose code and just a direct call to myFunction somewhere in the code?" and the answer is "this is a better approach because the callback is memoized (= will be taken in care by React that caches some operation to increase performances) and will change only if myFunction changes (I mean if you use another function because you have to fetch from another route)".
useCallback is used to prevent useless re-rendering of components or its child. If you know about React.memo(), useCallback is its functional equivalent.
Consider this:
const Foo = () => {
const handleClick = () => {
console.log('Clicked');
}
return <button onClick={handleClick}>Click Me</button>;
}
This will re-render the Foo component again and again even when it's not necessary.
Now consider this:
const Foo = () => {
const memoizedHandleClick =
useCallback(
() => console.log('Click happened')
,[]); // Tells React to memoize regardless of arguments.
return <Button onClick={memoizedHandleClick}>Click Me</Button>;
}
In this code, React will memoize the callback function and the callback will not be created multiple times hence no more useless re-renders
I have 2 actions in redux (both async) and I'm calling them both within my functional component via dispatch; the first using useEffect and the second via a button click. What I want to do is dispatch the actions to retrieve them from an async function, then use them within my component via useState. But using the useState is not rendering.
Here is my component:
export default function Hello()
{
const { first, second } = useSelector(state => state.myReducer);
const dispatch = useDispatch();
const fetchFirst = async () => dispatch(getFirst());
const fetchSecond = async () => dispatch(getSecond());
const fetchFixturesForDate = (date: Date) => dispatch(getFixturesForDate(date));
const [superValue, setSuperValue] = useState('value not set');
useEffect(() => {
const fetch = async () => {
fetchFirst();
setSuperValue(first);
};
fetch();
}, []);
const getSecondOnClickHandler = async () =>
{
console.log('a')
await fetchSecond();
setSuperValue(second);
}
return (
<div>
<p>The super value should first display the value "first item" once retrieved, then display "second value" once you click the button and the value is retrieved</p>
<p>Super Value: {superValue}</p>
<p>First Value: {first}</p>
<p>Second Value: {second}</p>
<button onClick={async () => await getSecondOnClickHandler()}>Get Second</button>
</div>
)
}
The superValue never renders even though I am setting it, although the value from first and second is retrieved and displayed.
StackBlitz.
Any help?
The value of first and second inside your two useEffects is set when the component mounts (I guess at that point they are undefined). So in both cases you will be setting superValue to that initial value.
You have two options:
Return the first/second values back from fetchFirst and fetchSecond, so that you can retrieve them directly from the executed function, and then set superValue:
useEffect(() => {
const fetch = async () => {
const newFirst = await fetchFirst();
setSuperValue(newFirst);
};
fetch();
}, []);
Add separate useEffects that listen for changes to first and second
useEffect(() => {
setSuperValue(first)
},[first])
useEffect(() => {
setSuperValue(second)
},[second])
The value in the reducer is not necessarily set when the action is dispatched, e.g. after fetchFirst() is called. Also the await that you do in await fetchSecond();
doesn't help since the reducer function is not executed.
You could add useEffect hooks and remove the setSuperValue from the other methods, but I think the code gets quite complicated.
What problem are you trying to solve in the first place?
useEffect(() => setSuperValue(first), [first]);
useEffect(() => setSuperValue(second), [second]);
useEffect(() => {
const fetch = async () => {
fetchFirst();
};
fetch();
}, []);
const getSecondOnClickHandler = async () => {
console.log('a');
await fetchSecond();
};
https://stackblitz.com/edit/react-ts-hsqd3x?file=Hello.tsx
I'm trying to test a simple hook i've made for intercepting offline/online events:
import { useEffect } from 'react';
const useOfflineDetection = (
setOffline: (isOffline: boolean) => void
): void => {
useEffect(() => {
window.addEventListener('offline', () => setOffline(true));
window.addEventListener('online', () => setOffline(false));
return () => {
window.removeEventListener('offline', () => setOffline(true));
window.removeEventListener('online', () => setOffline(false));
};
}, []);
};
export default useOfflineDetection;
------------------------------------
//...somewhere else in the code
useOfflineDetection((isOffline: boolean) => Do something with 'isOffline');
But I'm not sure I'm using the correct way to return value and moreover I'm not sure to get how to test it with jest, #testing-library & #testing-library/react-hooks.
I missunderstand how to mount my hook and then catch the return provide by callback.
Is someone can help me ? I'm stuck with it :'(
Thanks in advance!
EDIT:
Like Estus Flask said, I can use useEffect instead callback like I design it first.
import { useEffect, useState } from 'react';
const useOfflineDetection = (): boolean => {
const [isOffline, setIsOffline] = useState<boolean>(false);
useEffect(() => {
window.addEventListener('offline', () => setIsOffline(true));
window.addEventListener('online', () => setIsOffline(false));
return () => {
window.removeEventListener('offline', () => setIsOffline(true));
window.removeEventListener('online', () => setIsOffline(false));
};
}, []);
return isOffline;
};
export default useOfflineDetection;
------------------------------------
//...somewhere else in the code
const isOffline = useOfflineDetection();
Do something with 'isOffline'
But if I want to use this hook in order to store "isOffline" with something like redux or other, the only pattern I see it's using useEffect:
const isOffline = useOfflineDetection();
useEffect(() => {
dispatch(setIsOffline(isOffline));
}, [isOffline])
instead of just:
useOfflineDetection(isOffline => dispatch(setIsOffline(isOffline)));
But is it that bad ?
The problem with the hook is that clean up will fail because addEventListener and removeEventListener callbacks are different. They should be provided with the same functions:
const setOfflineTrue = useCallback(() => setOffline(true), []);
const setOfflineFalse = useCallback(() => setOffline(false), []);
useEffect(() => {
window.addEventListener('offline', setOfflineTrue);
...
Then React Hooks Testing Library can be used to test a hook.
Since DOM event targets have determined behaviour that is supported by Jest DOM to some extent, respective events can be dispatched to test a callback:
const mockSetOffline = jest.fn();
const wrapper = renderHook(() => useOfflineDetection(mockSetOffline));
expect(mockSetOffline).not.toBeCalled();
// called only on events
window.dispatchEvent(new Event('offline'));
expect(mockSetOffline).toBeCalledTimes(1);
expect(mockSetOffline).lastCalledWith(false);
window.dispatchEvent(new Event('online'));
expect(mockSetOffline).toBeCalledTimes(2);
expect(mockSetOffline).lastCalledWith(true);
// listener is registered once
wrapper.rerender();
expect(mockSetOffline).toBeCalledTimes(2);
window.dispatchEvent(new Event('offline'));
expect(mockSetOffline).toBeCalledTimes(3);
expect(mockSetOffline).lastCalledWith(false);
window.dispatchEvent(new Event('online'));
expect(mockSetOffline).toBeCalledTimes(4);
expect(mockSetOffline).lastCalledWith(true);
// cleanup is done correctly
window.dispatchEvent(new Event('offline'));
window.dispatchEvent(new Event('online'));
expect(mockSetOffline).toBeCalledTimes(4);
Handle scroll event will fire to often. What is the way to slowdown/debounce it?
And if it's possible, i want last event always be fired and not skipped.
const handleScroll = event => {
//how to debounse scroll change?
//if you will just setValue here, it's will lag as hell on scroll
}
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
Here is the useDebounce hook example from xnimorz
import { useState, useEffect } from 'react'
export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(
() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
},
[value, delay]
)
return debouncedValue
}
Event handler that uses hooks can be debounced done the same way as any other function with any debounce implementation, e.g. Lodash:
const updateValue = _.debounce(val => {
setState(val);
}, 100);
const handleScroll = event => {
// process event object if needed
updateValue(...);
}
Notice that due to how React synthetic events work, event object needs to be processed synchronously if it's used in event handler.
last event always be fired and not skipped
It's expected that only the last call is taken into account with debounced function, unless the implementation allows to change this, e.g. leading and trailing options in Lodash debounce.
const debounceLoadData = useCallback(debounce((debounceValue)=>{
props.setSenderAddress(debounceValue)
}, 300), []);