I am trying to implement custom global state hook based on the article here State Management with React Hooks — No Redux or Context API. I keep getting double renders. It seems to be with the following piece of code:
function useCustom() {
const newListener = useState()[1];
effect(() => {
this.listeners.push(newListener);
return () => {
this.listeners = this.listeners.filter(
listener => listener !== newListener
);
};
}, []);
return [this.state, this.setState, this.actions];
}
If you console log inside this piece of code you can see it running twice at initial render and also twice every time you update the hook.
Any help on how to fix this would be much appreciated.
Here is the full code:
CodeSandbox
import React, { useState, useEffect, useLayoutEffect } from "react";
const effect = typeof window === "undefined" ? useEffect : useLayoutEffect;
function setState(newState) {
if (newState === this.state) return;
this.state = newState;
this.listeners.forEach(listener => {
listener(this.state);
});
}
function useCustom() {
const newListener = useState()[1];
effect(() => {
this.listeners.push(newListener);
return () => {
this.listeners = this.listeners.filter(
listener => listener !== newListener
);
};
}, []);
return [this.state, this.setState, this.actions];
}
function associateActions(store, actions) {
const associatedActions = {};
if (actions) {
Object.keys(actions).forEach(key => {
if (typeof actions[key] === "function") {
associatedActions[key] = actions[key].bind(null, store);
}
if (typeof actions[key] === "object") {
associatedActions[key] = associateActions(store, actions[key]);
}
});
}
return associatedActions;
}
const useGlobalHook = (initialState, actions) => {
const store = { state: initialState, listeners: [] };
store.setState = setState.bind(store);
store.actions = associateActions(store, actions);
return useCustom.bind(store, React);
};
export default useGlobalHook;
Then set up the store like so:
import useGlobalState from './useGlobalState';
const initialState = false;
const useValue = useGlobalState(initialState);
export default useValue;
And the component
import React from 'react';
import useValue from '../store/useValue';
const Component1 = () => {
const [value, setValue] = useValue();
console.log('rendered component');
return (
<div>
<p>Value1: {value ? 'true' : 'false'}</p>
<button onClick={() => setValue(!value)}>Toggle Me</button>
</div>
);
};
export default Component1;
Related
I am developing a context in which through a function I can send "pokemons" to a global array, and also send the information of this array to my localstorage so that it is saved in the browser, I managed to do that and the array items are in localstorage, but every time the site refreshes, localstorage goes back to the empty array.
import React, { useEffect, useState } from "react";
import CatchContext from "./Context";
const CatchProvider = ({ children }) => {
const [pokemons, setPokemons] = useState([], () => {
const dataStorage = localStorage.getItem('pokemons');
if (dataStorage) {
return JSON.parse(dataStorage)
} else {
return [];
}
});
useEffect(() => {
localStorage.setItem('pokemons', JSON.stringify(pokemons));
}, [pokemons]);
const updatePokemons = (name) => {
const updatedPokemons = [...pokemons];
const pokemonsIndex = pokemons.indexOf(name);
if (pokemonsIndex >= 0) {
updatedPokemons.slice(pokemonsIndex, 1)
} else {
updatedPokemons.push(name)
};
setPokemons(updatedPokemons)
}
const deletePokemon = async (name) => {
await pokemons.splice(pokemons.indexOf(toString(name)))
}
return (
<CatchContext.Provider value={{ pokemons: pokemons, updatePokemons: updatePokemons, deletePokemon: deletePokemon }}>
{children}
</CatchContext.Provider>
);
}
export default CatchProvider;
The problem is that useState doesn't take two arguments.
Instead of:
const [pokemons, setPokemons] = useState([], () => {
You want:
const [pokemons, setPokemons] = useState(() => {
I think you don't need to call useEffect on initial render so you can make use of refs for this
import { useEffect, useRef } from "react";
// other code....
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) {
localStorage.setItem('pokemons', JSON.stringify(pokemons));
} else {
didMount.current = true;
}
}, [pokemons]);
There's a solution here... Several, actually - but none of them work for React 17.0.2. They all result in
Error: Rendered more hooks than during the previous render.
Even with fixes listed in the comments (Using useref() instead of useState, for instance).
So my question is - how can I have long click/press/tap in React 17.0.2 and newer?
My attempt at fixing it:
//https://stackoverflow.com/questions/48048957/react-long-press-event
import {useCallback, useRef, useState} from "react";
const useLongPress = (
onLongPress,
onClick,
{shouldPreventDefault = true, delay = 300} = {}
) => {
//const [longPressTriggered, setLongPressTriggered] = useState(false);
const longPressTriggered = useRef(false);
const timeout = useRef();
const target = useRef();
const start = useCallback(
event => {
if (shouldPreventDefault && event.target) {
event.target.addEventListener("touchend", preventDefault, {
passive: false
});
target.current = event.target;
}
timeout.current = setTimeout(() => {
onLongPress(event);
//setLongPressTriggered(true);
longPressTriggered.current = true;
}, delay);
},
[onLongPress, delay, shouldPreventDefault]
);
const clear = useCallback(
(event, shouldTriggerClick = true) => {
timeout.current && clearTimeout(timeout.current);
shouldTriggerClick && !longPressTriggered && onClick(event);
//setLongPressTriggered(false);
longPressTriggered.current = false;
if (shouldPreventDefault && target.current) {
target.current.removeEventListener("touchend", preventDefault);
}
},
[shouldPreventDefault, onClick, longPressTriggered]
);
return {
onMouseDown: e => start(e),
onTouchStart: e => start(e),
onMouseUp: e => clear(e),
onMouseLeave: e => clear(e, false),
onTouchEnd: e => clear(e)
};
};
const isTouchEvent = event => {
return "touches" in event;
};
const preventDefault = event => {
if (!isTouchEvent(event)) return;
if (event.touches.length < 2 && event.preventDefault) {
event.preventDefault();
}
};
export default useLongPress;
RandomItem.js:
import React, {useEffect, useState} from 'react';
import Item from "../components/Item";
import Loader from "../../shared/components/UI/Loader";
import {useAxiosGet} from "../../shared/hooks/HttpRequest";
import useLongPress from '../../shared/hooks/useLongPress';
function RandomItem() {
let content = null;
let item = useAxiosGet('collection');
if (item.error === true) {
content = <p>There was an error retrieving a random item.</p>
}
if (item.loading === true) {
content = <Loader/>
}
if (item.data) {
const onLongPress = useLongPress();
return (
content =
<div>
<h1 className="text-6xl font-normal leading-normal mt-0 mb-2">{item.data.name}</h1>
<Item name={item.data.name} image={item.data.filename} description={item.data.description}/>
</div>
)
}
return (
<div>
{content}
</div>
);
}
export default RandomItem;
The (unedited) useLongPress function should be used similar to the following example:
import React, { useState } from "react";
import "./styles.css";
import useLongPress from "./useLongPress";
export default function App() {
const [longPressCount, setlongPressCount] = useState(0)
const [clickCount, setClickCount] = useState(0)
const onLongPress = () => {
console.log('longpress is triggered');
setlongPressCount(longPressCount + 1)
};
const onClick = () => {
console.log('click is triggered')
setClickCount(clickCount + 1)
}
const defaultOptions = {
shouldPreventDefault: true,
delay: 500,
};
const longPressEvent = useLongPress(onLongPress, onClick, defaultOptions);
return (
<div className="App">
<button {...longPressEvent}>use Loooong Press</button>
<span>Long press count: {longPressCount}</span>
<span>Click count: {clickCount}</span>
</div>
);
}
Be sure to pass in the onLongPress function, onClick function, and the options object.
Here is a codesandbox with React 17.0.2 with a working example of useLongPress: https://codesandbox.io/s/uselongpress-forked-zmtem?file=/src/App.js
I am checking to see if isFetchingData then don't render yet but its not re-rendering once isFetchingData is set to false. I have the useEffect in the context and i would hope that would re-render once isFetchingData is set to false. Any ideas?
When I refresh the page it renders with the data. So I think its to do with re-rendering.
I am using react context to get the data and exposing functions to filter that data and get me what i need.
Context:
import React, { useEffect, useState } from 'react';
import getAllEmployees from 'my-services/employee/getAllEmployees';
import { arrayOf, node, oneOfType } from 'prop-types';
export const EmployeeContext = React.createContext({
allEmployees: [],
getActiveEmployees: () => [],
getTerminatedEmployees: () => []
});
const EmployeesProvider = ({ children }) => {
const [isFetchingData, setIsFetchingData] = useState(true);
const [allEmployees, setAllEmployees] = useState({});
useEffect(() => {
getAllEmployees().then(
//doing something
).then(employees => {
setAllEmployees(employees);
setIsFetchingData(false);
});
}, [isFetchingData])
const context = {
isFetchingData,
allEmployees,
getActiveEmployees: () =>
allEmployees.filter(x => x.status === 'Current'),
getTerminatedEmployees: () =>
allEmployees.filter(x => x.status === 'Terminated')
};
return (
<EmployeeContext.Provider value={context}>{children}</EmployeeContext.Provider>
);
};
EmployeesProvider.propTypes = {
children: oneOfType([node, arrayOf(node)])
};
EmployeesProvider.defaultProps = {
children: undefined
};
export default EmployeesProvider;
Component:
import React, { useContext } from 'react';
import styled from 'styled-components';
import { EmployeeContext } from 'my-contexts/EmployeeContext';
import EmployeeCard from '../../../components/EmployeeCard';
const EmployeesTab = () => {
const {
getActiveEmployees,
getTerminatedEmployees,
isFetchingData
} = useContext(EmployeeContext);
let activeEmployees = [];
let terminatedEmployees = [];
if (!isFetchingData) {
activeEmployees = getActiveEmployees();
terminatedEmployees = getTerminatedEmployees();
}
if(isFetchingData) {
return <p>Loading</p>;
}
return (
<Outer>
<TopHeader>
<H3>Employees ({activeEmployees.length})</H3>
</TopHeader>
<Wrapper>
{activeEmployees.map(employee => {
return (
<EmployeeCard
id={employee.id}
guid={employee.guid}
firstName={employee.name.first}
lastName={employee.name.last}
jobTitle={employee.jobTitle}
/>
);
})}
</Wrapper>
<H3>Terminated employees({terminatedEmployees.length})</H3>
<Wrapper>
{terminatedEmployees.map(employee => {
return (
<EmployeeCard
id={employee.id}
guid={employee.guid}
firstName={employee.name.first}
lastName={employee.name.last}
jobTitle={employee.jobTitle}
/>
);
})}
</Wrapper>
</Outer>
);
};
export default EmployeesTab;
I think many problems may exist.
At first, please check whether whole component is closed by context Provider.
For example
<EmployeesProvider>
<EmployeesTab/>
<EmployeesProvider/>
Please check this problem.
What is the correct way to create redux store within useEffect?
I have a use case that a child App.js will be exported as web-component (e.g. a javascript file), then embed (e.g. reference this file in html header) by a parent react app.
One flow is that the parent app will render App.js twice or N times. I put store creation inside useEffect + empty dependency, so this will be called once, regardless how many times the parent app renders the child app.
Once my store is ready, I render rest of the component + pass the store to context.
In Component.js, I look up the state and want to use them.
The error I have:
https://github.com/facebookincubator/redux-react-hook/blob/master/src/create.ts
const state = store.getState();, getState does not exist. It seems that either store is not ready when I tried to look up the state.
So I wonder what is the correct way to create store within useEffect?
App.js
export default function App() {
const store = useRef(null);
const [storeReady, setStoreReady] = useState(false);
const [token, setToken] = useState('');
useEffect(() => {
store.current = createReduxStore();
setStoreReady(true);
return () => {
store.current = null;
};
}, []);
useEffect(() => {
if (storeReady) {
// get token and set in state
token(auth())
}
}, [auth, storeReady]);
// store.current
if (store.current !== null) {
//test
console.log(
'++++++ store.current not null, condi',
store.current !== null,
store.current
);
return (
<>
<StoreContext.Provider value={store.current}>
<MyApp
token={token}
/>
</StoreContext.Provider>
</>
);
} else {
return null;
}
}
Store.js
export function createReduxStore(initialState = {}) {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
return store;
}
Component.js
export default function MyApp() {
const mapState = useCallback(
state => ({
history:
state !== undefined && state.idv !== undefined ? state.idv.history : [],
status:
state !== undefined && state.idv !== undefined
? state.idv.status
: initialStatus,
error:
state !== undefined && state.idv !== undefined ? state.idv.error : ''
}),
[]
);
const {history, status, error} = useMappedState(mapState);
}
// https://github.com/facebookincubator/redux-react-hook/blob/master/src/create.ts
function useMappedState<TResult>(
mapState: (state: TState) => TResult,
equalityCheck: (a: TResult, b: TResult) => boolean = defaultEqualityCheck,
): TResult {
const store = useContext(StoreContext);
if (!store) {
throw new MissingProviderError();
}
// We don't keep the derived state but call mapState on every render with current state.
// This approach guarantees that useMappedState returns up-to-date derived state.
// Since mapState can be expensive and must be a pure function of state we memoize it.
const memoizedMapState = useMemo(() => memoizeSingleArg(mapState), [
mapState,
]);
// getState is undefined
const state = store.getState();
const derivedState = memoizedMapState(state);
Update 1, but not working #backtick
export default function App() {
const duckStore = { getState() {} };
const store = useRef(duckStore);
const [token, setToken] = useState('');
useEffect(() => {
store.current = createReduxStore();
return () => {
store.current = duckStore;
};
}, []);
// store.current
if (store.current !== null) {
return (
<>
<StoreContext.Provider value={store.current}>
<MyApp />
</StoreContext.Provider>
</>
);
} else {
return null;
}
}
Store.js
export function createReduxStore(initialState = {}) {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
return store;
}
Component.js
export default function MyApp() {
// There is no store variable here, useMappedState is able to get the store value.
const mapState = useCallback(
state => ({
history:
state !== undefined && state.idv !== undefined ? state.idv.history : [],
status:
state !== undefined && state.idv !== undefined
? state.idv.status
: initialStatus,
error:
state !== undefined && state.idv !== undefined ? state.idv.error : ''
}),
[]
);
const {history, status, error} = useMappedState(mapState);
}
useMappedState calls the store.getState() on every render, even the first one when the store isn't ready. Just duck-type the store ref's initial value:
// module scope
const duckStore = { getState() {} };
// in component
const store = useRef(duckStore);
// in useEffect cleanup
return () => { store.current = duckStore; };
I need help because I get the following error: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method. in createCategory (at themeProvider.js:39)
/* Imports */
import React, { useContext, useState, useEffect } from 'react';
import AsyncStorage from '#react-native-community/async-storage';
import THEMES from '#app/theme/themes.json';
/* /Imports/ */
const STORAGE_KEY = 'THEME_ID';
const ThemeContext = React.createContext();
/* Exports */
export const ThemeContextProvider = ({ children }) => {
const [themeID, setThemeID] = useState();
useEffect(() => {
(async () => {
const storedThemeID = await AsyncStorage.getItem(STORAGE_KEY);
if (storedThemeID) setThemeID(storedThemeID);
else setThemeID(THEMES[1].key);
})();
}, []);
return (
<ThemeContext.Provider value={{ themeID, setThemeID }}>
{!!themeID ? children : null}
</ThemeContext.Provider>
);
};
export function withTheme(Component) {
function TargetComponent(props) {
const { themeID, setThemeID } = useContext(ThemeContext);
const getTheme = themeID => THEMES.find(theme => theme.key === themeID);
const setTheme = themeID => {
AsyncStorage.setItem(STORAGE_KEY, themeID);
setThemeID(themeID);
};
return (
<Component
{...props}
themes={THEMES}
theme={getTheme(themeID)}
setTheme={setTheme}
/>
);
}
TargetComponent.navigationOptions = Component.navigationOptions;
return TargetComponent;
}
/* /Exports/ */
If you don't already know - you can return a function at the end of your useEffect hook. That function will be called whenever that effect is fired again (e.g. when the values of its dependencies have changed), as well as right before the component unmounts. So if you have a useEffect hook that looks like this:
useEffect(() => {
// logic here
return () => {
// clean up
};
}, []); // no dependencies!
Is equivalent to this:
class SomeComponent extends React.Component {
componentDidMount() {
// logic here
}
componentWillUnmount() {
// clean up
}
}
So in your code I'd add this:
useEffect(() => {
let isCancelled = false;
const fetchData = async () => {
try {
// fetch logic omitted...
const data = await AsyncStorage.getItem(STORAGE_KEY);
if (storedThemeID) setThemeID(storedThemeID);
else setThemeID(THEMES[1].key);
} catch (e) {
throw new Error(e)
}
};
fetchData();
return () => {
isCancelled = true;
};
}, [themeID]);
Try this
let unmounted = false;
useEffect(() => {
(async () => {
const storedThemeID = await AsyncStorage.getItem(STORAGE_KEY);
if (!unmounted) {
if (storedThemeID) setThemeID(storedThemeID);
else setThemeID(THEMES[1].key);
}
})();
return () => {
unmounted = true;
};
}, []);