React data content is disappearing on callback - reactjs

there is something strange happening with my code. My variable data (useState) is randomly empty when I call my callback when onpopstate event is fired.
I have 2 components and 1 hook used like that:
const Parent = props => {
const {downloadData} = useData();
const [data, setData] = useState([]);
const [filteredData, setFilteredData] = useState();
const loadData = async () => setData(await downloadData());
useEffect(() => {
loadData();
}, []);
return <FilterPage data={data} onDataChange={data => setFilteredData(data)} />
}
const FilterPage = ({data, onDataChange} => {
const {saveHistoryData} = useHistoryState('filter', null, () => {
updateFilters();
});
const filter = (filterData, saveHistory = true) => {
let r = data; // data is randomly empty here
...
if(saveHistory)saveHistoryData(filterData);
onDataChange(r);
}
});
// my hook
const useHistoryState = (name, _data, callback) => {
const getHistoryData = () => {
const params = new URLSearchParams(window.location.search);
try{
return JSON.parse(params.get(name));
}catch(err){
return null;
}
}
const saveHistoryData = (data) => {
const params = new URLSearchParams(window.location.search);
params.set(name, JSON.stringify(data || _data));
window.history.pushState(null, '', window.location.pathname + '?' + params.toString());
}
const removeHistoryData = () => {
const params = new URLSearchParams(window.location.search);
params.delete(name);
window.history.pushState(null, '', window.location.pathname + '?' + params.toString());
}
const watchCallback = () => {
callback(getHistoryData());
};
useEffect(() => {
let d = getHistoryData();
if(d)watchCallback();
window.addEventListener('popstate', watchCallback);
return () => window.removeEventListener('popstate', watchCallback);
}, []);
return {getHistoryData, saveHistoryData, removeHistoryData};
}
Any suggestions please
Edit
I'm sorry is not the entire code, just a draft. I download the data using async function. The data is loading fine but is empty only if we call the callback from the hook.

You need to use setData to populate data

First of all you are not calling setData() anywhere.
You are using data but not setData and you are using setFilteredData but not filteredData.
Furthermore it doesn't look like updateFilters() exist within FilterPage.
You are passing onDataChange to <Filterpage> but you are not using the property, only ({data}) which explains why it's empty. You might want to update the FilterPage signature: const FilterPage = ({data, onDataChange}) => {} and use the onDataChange

Related

How to immediately update a state in React

I'm struggling with this problem and I've already tried many solutions but none of them fit me.
I have a context that I use to share information that I get from an API. I will summarize the files for you:
file: useGetInfo.tsx
type InfoContextData = { ... }
type Props = { ... }
type InfoResponseProps = { ... }
export const InfoContext = createContext<InfoContextData>({} as InfoContextData)
export const InformationProvider = ({ children }: Props) => {
const isBrowser = typeof window !== `undefined`
const [infoStorage, setInfoStorage] = useState(
isBrowser && localStorage.getItem('info')
? String(localStorage.getItem('info'))
: undefined
)
const [result, setResult] = useState<InfoResponseProps | null>(null)
const getInfo = useCallback(async (value: string) => {
const url = `<URL_FROM_API${value}>`
await axios.get(url)
.then((response) => {
setResult(response.data)
})
.catch((_) => {
setResult(null)
})
})
useEffect(() => {
if (!infoStorage) {
return
}
getInfo(infoStorage)
}, [infoStorage, getInfo])
return (
<InfoContext.Provider
value={{
result,
setResult,
infoStorage,
setInfoStorage,
getInfo,
}}
>
{children}
</InfoContext.Provider>
)
}
Then in the component I call the context:
file: SomeComponent.tsx
const Component = () => {
const { setInfoStorage, getInfo, result } = useContext(InfoContext)
const [input, setInput] = useState('')
const handleInfoSubmit = useCallback(() => {
getInfo(input)
if (!result || !result?.ok) {
localStorage.removeItem('info')
setInfoStorage(undefined)
}
setInfoStorage(input)
localStorage.setItem('info', 'input')
setInput('')
}, [input, result, getInfo, setInfoStorage, setInput])
return (
...
<Form onSubmit={handleInfoSubmit}>
<input>
...
</Form>
)
}
Basically, the user inserts a code in the form and when he submits the form, it runs the handleInfoSubmit function. Then, the code runs the function getInfo() and after requesting the API it returns the information to the state result.
The problem is in the SomeComponent.tsx file: when I run the function getInfo(input) I need the information in the state result but at the time axios finishes the request to the API and the code goes to the if (!result || !result?.ok) line, the result state is not still fulfilled.
I know that React/Gatsby can't update immediately the state like what I need, but is there a way to overcome this problem? Thanks in advance.
I think the value of the result would always be stale inside the handleInfoSubmit function per your code.
Rewrite the getInfo and handleInfoSubmit like this
// Return data from getInfo so that we can use the value directly in handleInfoSubmit
const getInfo = useCallback(async (value: string) => {
const url = `<URL_FROM_API${value}>`
try {
const { data } = await axios.get(url);
setResult(data)
return data;
} catch {
setResult(null)
}
return null;
})
const handleInfoSubmit = useCallback(async () => {
// await getInfo and get the axios response data.
const result = await getInfo(input)
if (!result || !result?.ok) {
localStorage.removeItem('info')
setInfoStorage(undefined)
}
setInfoStorage(input)
localStorage.setItem('info', 'input')
setInput('')
}, [input, getInfo, setInfoStorage, setInput])

How do I place this object inside an array

How do I place this object inside an array: Dog API. Is an object inside another object. I'm trying to set setBreeds(breedsList.message) but does not work.
const basicUrl = `https://dog.ceo/api/breeds/`
const listUrl = `list/all`
const Home = () => {
// uses state to store the list of breeds
const [breeds, setBreeds] = useState([])
// fetch the list of breeds
const fetchBreeds = async () => {
let url
url = `${basicUrl}${listUrl}`
const response = await fetch(url)
const breedsList = await response.json()
setBreeds(breedsList)
}
// useeffect to mount the fetchBreeds function
useEffect(() => {
fetchBreeds()
}, [])
return (
<div>
{/* // maps */}
{breeds.map((breed) => console.log(breed))}
</div>
)
}
export default Home
You do like this
setBreeds(prevState => [...prevState, breedsList.message])
I'm not sure if that is what you are asking for but the following function turns the json response into a list of breed names. You could transform the object and then call setBreeds.
function transformToList(breedsResponse) {
const {message: breeds} = breedsResponse;
const breedMainNames = Object.keys(breeds);
return breedMainNames.reduce((acc, mainName) => {
const subNames = breeds[mainName];
if(subNames.length === 0) {
acc.push(mainName)
} else {
const combinedNames = subNames.map(name => `${name} ${mainName}`);
acc.push(...combinedNames);
}
return acc;
}, [])
};

useEffect/componentWillUnmount fires but state is empty

I have a hook component that allows a user to upload a set of images. I want to set it up in such a way that when the component is un mounted that all the files are uploaded to my backend.
currently using useEffect with a return of a function as the componentWillUnmount substitute, however when the function is called the state that it requires (the set of files uploaded) is empty i.e. empty list. Is there a way to fix this or a better way to do it? I suspect its because the useState for the stagedUploadedImages is set to an empty list. Its not an option to lift the state out of this component.
const [stagedUploadedImages, setStagedUploadedImages] = useState([]);
const uploadStagedFiles = () => {
// when this is reached by the useEffect method `stagedUPloadedImages` is empty list
stagedUploadedImages.forEach((file) => {
const formData = new FormData();
formData.append("files", file);
api.uploads.uploadWithNoAssociation(formData).then((response) => {
console.log("ImageGallery: response: ", response);
});
});
};
useEffect(() => {
return () => {
uploadStagedFiles();
};
}, []);
const handleUpload = (files) => {
setStagedUploadedImages([...files]);
};
Explanation: https://www.timveletta.com/blog/2020-07-14-accessing-react-state-in-your-component-cleanup-with-hooks/
const [stagedUploadedImages, setStagedUploadedImages] = useState([]);
const valueRef = useRef();
const uploadStagedFiles = () => {
valueRef.current.forEach((file) => {
const formData = new FormData();
formData.append("files", file);
api.uploads.uploadWithNoAssociation(formData).then((response) => {
console.log("ImageGallery: response: ", response);
});
});
};
useEffect(() => {
valueRef.current = stagedUploadedImages;
}, [stagedUploadedImages]);
useEffect(() => {
return () => {
uploadStagedFiles();
};
}, []);
Additional info: https://dmitripavlutin.com/react-hooks-stale-closures/

How to get the current state inside socket.io on callback function

const useChat = () => {
const [messages, setMessages] = useState([]);
const socketRef = useRef();
const { chatId } = useSelector(state => state.chatin)
const { chatList } = useSelector(state => state.chatin)
const dispatch = useDispatch()
useEffect(() => {
socketRef.current = io(socketClient);
socketClient.on('chat', (data) => {
const targetMessage = (messages) => messages.findIndex(item => item.message_number === data.message_number);
console.log('targetMessage', targetMessage)
if (targetMessage !== -1) {
messages[targetMessage].is_hide = true;
}
setMessages((messages) => [...messages, data]);
});
return () => {
socketRef.current.disconnect();
};
}, []);
whenever I got new socket data, I wanna change 'messages' data, but can't access it, because it always shows initial data value.After that I have a question about how can I set it?
You can move the if condition inside setMessages function, this way you will get access to the current state:
socketClient.on('chat', (data) => {
setMessages((messages) => {
const targetMessage = messages.findIndex(item => item.message_number === data.message_number);
if (targetMessage !== -1) {
messages[targetMessage].is_hide = true;
}
return [...messages, data]
});
});

How can I initialize in useState with the data from custom hooks?

I'm learning to React Hooks.
And I'm struggling initialize data that I fetched from a server using a custom hook.
I think I'm using hooks wrong.
My code is below.
const useFetchLocation = () => {
const [currentLocation, setCurrentLocation] = useState([]);
const getCurrentLocation = (ignore) => {
...
};
useEffect(() => {
let ignore = false;
getCurrentLocation(ignore);
return () => { ignore = true; }
}, []);
return {currentLocation};
};
const useFetch = (coords) => {
console.log(coords);
const [stores, setStores] = useState([]);
const fetchData = (coords, ignore) => {
axios.get(`${URL}`)
.then(res => {
if (!ignore) {
setStores(res.data.results);
}
})
.catch(e => {
console.log(e);
});
};
useEffect(() => {
let ignore = false;
fetchData(ignore);
return () => {
ignore = true;
};
}, [coords]);
return {stores};
}
const App = () => {
const {currentLocation} = useFetchLocation();
const {stores} = useFetch(currentLocation); // it doesn't know what currentLocation is.
...
Obviously, it doesn't work synchronously.
However, I believe there's the correct way to do so.
In this case, what should I do?
I would appreciate if you give me any ideas.
Thank you.
Not sure what all the ignore variables are about, but you can just check in your effect if coords is set. Only when coords is set you should make the axios request.
const useFetchLocation = () => {
// Start out with null instead of an empty array, this makes is easier to check later on
const [currentLocation, setCurrentLocation] = useState(null);
const getCurrentLocation = () => {
// Somehow figure out the current location and store it in the state
setTimeout(() => {
setCurrentLocation({ lat: 1, lng: 2 });
}, 500);
};
useEffect(() => {
getCurrentLocation();
}, []);
return { currentLocation };
};
const useFetch = coords => {
const [stores, setStores] = useState([]);
const fetchData = coords => {
console.log("make some HTTP request using coords:", coords);
setTimeout(() => {
console.log("pretending to receive data");
setStores([{ id: 1, name: "Store 1" }]);
}, 500);
};
useEffect(() => {
/*
* When the location is set from useFetchLocation the useFetch code is
* also triggered again. The first time coords is null so the fetchData code
* will not be executed. Then, when the coords is set to an actual object
* containing coordinates, the fetchData code will execute.
*/
if (coords) {
fetchData(coords);
}
}, [coords]);
return { stores };
};
function App() {
const { currentLocation } = useFetchLocation();
const { stores } = useFetch(currentLocation);
return (
<div className="App">
<ul>
{stores.map(store => (
<li key={store.id}>{store.name}</li>
))}
</ul>
</div>
);
}
Working sandbox (without the comments) https://codesandbox.io/embed/eager-elion-0ki0v

Resources