I have a 3 files:
Main component,
File with states that are stored in local storage
A file with a reset function for resetting these same states to default
values.
I import the file with the states and reset file in the main component and everything is ok. But when I try use reset function for set localState value to default, i got error “Error: Invalid hook call. Interceptors can only be called inside the body of a functional component. "
I read the documentation on react, but I did not understand the error
First file code:
import React from "react";
import { LocalStorage } from "./localState";
import { resetLocalStorage } from "./resetLocalState";
function App() {
const localState = LocalStorage(); // local storage keys
const resetState = () => resetLocalStorage(); // reset local storate states
return (
<div>
<button onClick={() => resetState()}>Refresh State to default</button>
<br />
<button
onClick={() => localState.setLocalStorageState("State was changed")}
>
Change State
</button>
<p>{localState.localStorageState}</p>
</div>
);
}
export default App;
Second file code:
import { useState, useEffect } from "react";
const useLocalStorageList = (key, defaultValue) => {
const stored = localStorage.getItem(key);
const initial = stored ? JSON.parse(stored) : defaultValue;
const [value, setValue] = useState(initial);
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
//local storage keys
export const LocalStorage = () => {
const [localStorageState, setLocalStorageState] = useLocalStorageList(
"State",
"Default Value"
);
return { localStorageState, setLocalStorageState };
};
Third file code
import { LocalStorage } from "./localState";
export const resetLocalStorage = () => {
const localState = LocalStorage(); //local storage keys
localState.setLocalStorageState("Default Value");
};
Link to SandBox
I didnt see anything to reset all states in your resetLocalStorage(). I assume you will keep track of all the 'local storage keys' and define reset-functions for each. This example modifies your hook to return a third function to reset the state so another reset-function doesn't have top be defined.
https://codesandbox.io/s/smoosh-sound-0yxvl?file=/src/App.js
I made few changes in your code to achieve the use case you were trying to achieve. (Let me know my implementation is suffices your use case)
The resetLocalStorage is not starting with use That's why you were getting the previous error.
After renaming that function to useResetLocalStorage, still it will not work since -
you were calling the custom hook onClick of button. This breaks one react hook rule which is react hooks must not be called conditionally https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
Related
I have created a reproducible exam of my problem, I don't understand why after the setDefaultValue is called and the component is updated (you can see it's updated using the result of my console.log) If now I click on the reset button instead of the new defaultValue I see the old one.
Here is a link to the example showing this problem, I'll also paste the code here
https://codesandbox.io/s/wonderful-tree-wtsgb4?file=/src/App.js
import "./styles.css";
import {useState, useRef} from 'react';
import TextBox from './TextBox';
export default function App() {
const textboxAPI = useRef(null)
const [defaultValue ,setDefaultValue] = useState('First')
return (
<div className="App">
<div style={{fontWeight: 'bold'}}>To reproduce please first click on the default value button then on the reset button</div>
<TextBox getAPI={(api) => textboxAPI.current = api} defaultValue={defaultValue}/>
<button onClick={() => setDefaultValue('second')}>1- Click me to change default value to "second"</button>
<button onClick={() => textboxAPI.current.reset()}>2- Click me to call reset inside Textbox</button>
</div>
);
}
import {useEffect, useState} from 'react';
const TextBox = ({defaultValue, getAPI}) => {
const [value, setValue] = useState(defaultValue || '')
useEffect(() => {
if (getAPI) {
getAPI({
reset: reset,
})
}
}, [])
const reset = () => {
console.log('TextBox Reset DefaultValue', defaultValue)
setValue(defaultValue)
}
console.log('TextBox DefaultValue', defaultValue)
return <div>{value}</div>
}
export default TextBox;
To reproduce the problem:
1- Click on the first button to set a new defaultValue, see the console.log, you can see the defaultValue has changed inside the TextBox Component
2- Click the reset button, it calls the reset function inside TextBox but the default value logged there has the previous value!
Here you save in textboxAPI.current function reset but just one time after first render of TextBox component. Function reset has a defaultValue in a closure and its value is 'First' during first render. So each next time you call textboxAPI.current.reset(), you call the reset function with defaultValue==='First' in its closure.
But you parent component controls child state and React does not recommend to manage your logic like that.
[UPDATED]
That will fix your issue, but I don not recommend to organize a state logic like that:
const TextBox = ({defaultValue, getAPI}) => {
const [value, setValue] = useState(defaultValue || '')
const reset = () => {
console.log('TextBox Reset DefaultValue', defaultValue)
setValue(defaultValue)
}
if (getAPI) {
getAPI({
reset: reset,
})
}
console.log('TextBox DefaultValue', defaultValue)
return <div>{value}</div>
}
export default TextBox;
Based on what I learned from the comments I tried using Hooks but There were too many changes needed especially I had some issues with React.lazy so I tried to look for more solutions until I found that using a combination of forwardRef and useImperativeHandle I can export my reset function without changing my current structure and it works as it should, I thought that I should share my solution for anyone else who might be looking for an alternative solution to Hooks.
How do I render based on the data in the localStorage? The localStorage has a key-value pair (location: US or Location: AU) and is added when the user visits.
import React, { useEffect, useState} from 'react';
let localUserLoc = userLoc; //works fine when I set this variable to 'US' or 'AU'
const LoadSections= (props) => {
const { sections, className } = props;
const [userLoc, setUserLoc] = useState(JSON.stringify(localStorage.getItem('userLoc')) || '');
useEffect(() => {
const userLoc = JSON.stringify(localStorage.getItem('userLoc'));
setUserLoc(userLoc);
}, []);
useEffect(() => {
getUserLoc = userLoc;
}, [userLoc]);
// // console.log(local1, 'outside');
console.log(abTestType, '4 - outside');
return (
<Wrapper className={className}>
{sections.map((section, sectionIndex) => {
const userLocProp = section.userLoc;
if (userLocProp === localUserLoc) {
return <SomeComp />
}
else if (userLocProp === 'undefined') {
return <AnotherComp />
}
The code above only loads the if I manually set the localUserLoc to 'US' or 'AU'. However, when I try to update the variable from localStorage it doesn't display .
I think it is because the LocalStorage is read but not rendered? I tried using two UseEffect hooks and added the first one as a dependency but to no avail.
How can I read and then set the localUserLoc variable to whatever it is in the localStorage and render the relevant component?
The user loc is detected via IP and then I setItem in localStorage.
It doesn't work because you are using JSON.stringify after getting the value from localStorage but the value stored in localStorage is already a string because localStorage can only store string values.
So you have to use JSON.parse for getting the value from localStorage and convert it to a javascript object.
const [userLoc, setUserLoc] = useState(JSON.parse(localStorage.getItem('userLoc'))
The issue I got is that the fetched Data from API is not saved to a variable. Please look at the fearvalue, it's being called later and the value of that is an empty string.
APY component
export let fearvalue = [];
export const fearAndGreed = () => {
// 1. Create a new XMLHttpRequest object
let bitcoinAPY = new XMLHttpRequest();
// 2. Configure it: GET-request for the URL /article/.../load
bitcoinAPY.open("GET", "https://api.alternative.me/fng/?limit=10&date_format=us", false)
bitcoinAPY.onload = () => {
const data = JSON.parse(bitcoinAPY.response);
/*const saveStaticDataToFile = () => {
let blob = new Blob(['Welcome'],
{type: 'text/plain;charset=utf-8'});
saveStaticDataToFile(blob, "static.txt")
}*/
console.log(data)
fearvalue = data.data[0];
}
// 3. Send the request over the network
bitcoinAPY.send();
}
window.addEventListener('load', fearAndGreed)
fearvalue is being called in this component and it is a blank value. Can anyone help me with saving data to this variable?
import './App.css';
import './Apy_TAPI';
import './Bitcoin Fear&Greed';
import { DataFormatting } from './DataFormatting.js';
import { fearvalue } from './Bitcoin Fear&Greed';
import './App.css';
import './Apy_TAPI';
import './Bitcoin Fear&Greed';
import { DataFormatting } from './DataFormatting.js';
import { fearvalue } from './Bitcoin Fear&Greed';
function App() {
const test1 = "test1"
console.log(fearvalue)
return (
<div className="App">
<header className="App-header">
<p>
Bitcoin analyst tool
</p>
</header>
<div className='Text'>
<h1>
<img className="Image" src="https://alternative.me/crypto/fear-and-greed-index.png" alt="Latest Crypto Fear & Greed Index" />
</h1>
<h2>
https://bitinfocharts.com/pl/bitcoin/address/1P5ZEDWTKTFGxQjZphgWPQUpe554WKDfHQ <br />
<br />
{fearvalue} <br />
</h2>
</div>
</div>
);
}
export default App;
You need to save the response in a proper way. in React.js, you could use useState to create a state variable and set the response in it.
First, you need to save the response in a state variable:
import React, {useState} from 'react';
export const fearAndGreed = () => {
const [fearValue, setFearValue] = useState() // initialize with proper value according to your data type, it could be a empty array [] or an object {}.
let bitcoinAPY = new XMLHttpRequest();
bitcoinAPY.open("GET", "https://api.alternative.me/fng/?limit=10&date_format=us", false)
bitcoinAPY.onload = () => {
const data = JSON.parse(bitcoinAPY.response);
setFearValue(data.data[0]) // ------> set the fearValue here
}
bitcoinAPY.send();
}
window.addEventListener('load', fearAndGreed)
So far, the first part is done. but you need the fearValue in the other component. to achieve this, there are some solutions like using a Global State Manager like Redux or ContextApi. without them, your implementation would be tricky since you can't use the lifting state up technique because you didn't use fearAndGreed as a child component in the parent component (App).
In such cases, you can implement a custom hook with fearAndGreed function. since the function invokes once after the page loading, you can implement this by calling the API after your components did mount.
Let's make a custom hook with the current fearAndGreed function in this way:
import {useEffect, useState} from 'react';
export const useFearAndGreed = () => {
const [fearValue, setFearValue] = useState();
useEffect(() => {
let bitcoinAPY = new XMLHttpRequest();
bitcoinAPY.open("GET", "https://api.alternative.me/fng/?limit=10&date_format=us", false)
bitcoinAPY.onload = () => {
const data = JSON.parse(bitcoinAPY.response);
setFearValue(data.data[0]) // ------> set the fearValue here
}
bitcoinAPY.send();
}, [])
return fearValue;
}
Explanation:
With a few changes, fearAndGreed function becomes a custom hook useFearAndGreed.
The API will call in the useEffect after the component did mount (with an empty array of dependencies).
The hook will return the fearValue on every change.
Now, time to use this custom hook inside of the App component:
function App() {
const fearValue = useFearAndGreed()
return (
<div>
{fearValue}
</div>
);
}
export default App;
Note: I removed the other parts of implementation in the App component to simplify it. you should include your own implementation within.
Now, every time the App component did mount, the useFearAndGreed will be invoked and return the fearValue which can be saved in a constant fearValue to use in the div element.
I might be not understanding completely the principals oh useState hook and there is a problem I came across a few times now.
I'm trying to set an initial state in my component using useEffect without dependencies and later on compare it to some other variables I get from redux.
// get the customer saved in redux
const Customer = useGetCustomer();
const [prevCustomerNumber, setPrevCustomerNumber] = useState(null)
useEffect(() => {
// if there is already customer saved in redux, set it as the previous customer on mount
const { CustomerNumber } = Customer || {}
setPrevCustomerNumber(CustomerNumber)
}, [])
const submit = () => {
//check if there is any change between the customer in redux and the previous one
const { CustomerNumber } = Customer || {}
const submitWithoutChanges = Customer && ( CustomerNumber === prevCustomerNumber)
submitCustomer(Customer, submitWithoutChanges )
}
My problem is - when clicking submit, prevCustomerNumber is always null.
My questions are:
Is it because useState runs again after the first useEffect and override it?
What should I do to save initial state properly?
you can set your initial state of prevCustomerNumber as
const [prevCustomerNumber, setPrevCustomerNumber] = useState("some value")
Your problem is that you get the customer from redux only when the component renders. When you click submit button, the function does not tries to read the data from redux again and only uses the data that already has which may be wrong or null. In order to have the latest data from redux store you need to wrap your component with the Connect function:
export default connect ((state) => {
const customerNumber = state.customer.customerNumber
return {customerNumber}
})(YourComponent)
YourComponent will now receive the customerNumber props every time your redux store changes.
Or if you prefer hooks you can use useSelector hook:
import { useSelector } from 'react-redux'
const customerNumber = useSelector(state => state.customer.customerNumber)
Now every times the components renders or the redux store changes the useSelector hook will return the update value
I have this file that I'm keeping a INITIAL_VALUE for a form field, that I'm building.
INITIAL_VALUE.js
const INITIAL_VALUE = [];
export default INITIAL_VALUE;
And the problem is that INITIAL_VALUE is an array. A non-primitive, that is handled by reference.
Component1.js
import INITIAL_VALUE from "./INITIAL_VALUE";
import React, { useState } from "react";
function Component1(props) {
const [myState, setMyState] = useState(INITIAL_VALUE);
const [boolean, setBoolean] = useState(false);
function handleClick() {
setMyState(prevState => {
prevState.push(1);
return prevState;
});
setBoolean(prevState => !prevState);
props.forceUpdateApp();
}
return (
<React.Fragment>
<div>This is my state: {JSON.stringify(myState)}</div>
<button onClick={handleClick}>Modify State Comp1</button>
</React.Fragment>
);
}
export default Component1;
Component2.js
The same as Component1, but it's named Component2 and it has its own file.
App.js
function App() {
const [boolean, setBoolean] = useState(false);
function forceUpdateApp() {
setBoolean(prevState => !prevState);
}
return (
<React.Fragment>
<Component1 forceUpdateApp={forceUpdateApp} />
<Component1 forceUpdateApp={forceUpdateApp} />
<Component2 forceUpdateApp={forceUpdateApp} />
</React.Fragment>
);
}
CodeSandbox
PROBLEM
Component1.js and Component2.js both import the INITIAL_VALUE file. And I was under the impression that, each one of these imports would get a brand new instance of the INITIAL_VALUE object. But that is not the case as we can see from the GIF below:
QUESTION
Is there a way to keep an array as a initial value living declared and imported from another file and always get a new reference to it on each import? Is there another pattern I can use to solve this? Or should I stick with only primitive values and make it null instead of [] and intialize it in the consumer file?
Is there a way to keep an array as a initial value living declared and imported from another file and always get a new reference to it on each import?
No, that's not possible. The top-most level code of a module will run once, at most. Here, the top level of INITIAL_VALUE.js defines one array and exports it, so everything that imports it will have a reference to that same array.
Easiest tweak would be to export a function which creates the array instead:
// makeInitialValue.js
export default () => {
const INITIAL_VALUE = [];
// the created array / object can be much more complicated, if you wish
return INITIAL_VALUE;
};
and then
import makeInitialValue from "./makeInitialValue";
function Component1(props) {
const INITIAL_VALUE = makeInitialValue();
const [myState, setMyState] = useState(INITIAL_VALUE);
In the simplified case that you just need an empty array, it would be easier just to define it when you pass it to useState.
All that said, it would be much better to fix your code so that it does not mutate the existing state. Change
setMyState(prevState => {
prevState.push(1);
return prevState;
});
to
setMyState(prevState => {
return [...prevState, 1];
});
That way, even if all components and component instances start out with the same array, it won't cause problems, because the array will never be mutated.