How to call a hook function into a map in react - reactjs

I'm using ketting for my React REST client.
The library provides hooks to access some data, In my case, this is this hook :
import { useResource } from 'react-ketting';
...
const { loading, error, data } = useResource('https://api.example/article/5');
I have multiples resources and I want to loop on them:
items.map(item => {
const { loading, error, data, resourceState } = useResource(item);
myItems.push({
title: data.name,
onClick: () => go(resourceState.links.get('self').href),
});
});
But React doesn't accept looping the useResource hook like this.
So I found a dirty solution that I'm not proud of...
import React from 'react';
import { useCollection, useResource } from 'react-ketting';
let myItems = [];
const Collection = ({ resource, go }) => {
const { items } = useCollection(resource);
myItems = [];
return (
<div>
{items.map(item => (
<CollectionItem go={go} resource={item} />
))}
<ElementsUser elements={myItems} />
</div>
);
};
const CollectionItem = ({ resource, go }) => {
const { data, resourceState } = useResource(resource);
myItems.push({
title: data.name,
onClick: () => go(resourceState.links.get('self').href),
});
return null;
};
Do you have any tips to deal with that problem?
And is it possible that it causes a :
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 a useEffect cleanup function.

Using 1 component per Resource is exactly the pattern that's recommended. I'd even say it's a best practice. Note that a call to useResource will not result in an API call if there was a cached resource state.
If you find that you are seeing 1 request per iteration, you might want to make sure that your collection returns every member as an embedded resource. If you use the useCollection hook, the request will include a Prefer: transclude=item header to give a hint to the server that embedding might be desired.
Also, react-ketting will do cleanups on unmount.

Related

How to access data from custom react hook

Preface: I'm fairly new to React (Coming over from Angular). I know things a similar but different.
I have referenced the following SO threads to no avail in my situation:
React not displaying data after successful fetch
Objects are not valid as a React child. If you meant to render a collection of children, use an array instead
Currently, I'm trying to get my data to display from an API I developed. I'm used to the Angular approach which would call for a ngFor in the template for most data showcase situations.
I'm having trouble wrapping my mind around what I have to do here in order to display my data. The data is expected to be an array of objects which I would then parse to display.
I also receive the following error: Error: Objects are not valid as a React child (found: object with keys {data}). If you meant to render a collection of children, use an array instead.
I've searched high and low for a solution but sadly, nothing I've seen has worked for me. (All of the answers on SO are using the class-based version of React, of which I am not).
You can see my data output in the following screenshot:
I am also including my custom hook code and the component that is supposed to render the data:
CUSTOM DATA FETCH HOOK
interface Drone{
id: number;
name: string;
model: string;
price: number;
}
export function useGetData(urlpath:string) {
const [droneData, setData] = useState<any>()
async function handleDataFetch(path:string){
const result = await fetch(`https://drone-collections-api-jc.herokuapp.com${path}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'x-access-token': 'Bearer API-TOKEN'
}
})
const response = await result.json();
setData(response)
}
useEffect( () => {
handleDataFetch(urlpath)
})
return droneData
}
THE DRONE COMPONENT
import { useGetData } from '../../custom-hooks'
export const Drones = () => {
let data = useGetData('/drones')
console.log(data)
// const DisplayDrone = ( ) => {
// return (
// Array.prototype.map( data => {
// <div>{ data.name }</div>
// })
// )
// }
return (
<div>
<h1>Hello Drones</h1>
</div>
)
}
Also, for more context, the current code can be found at this repo: https://github.com/carter3689/testing-drone-frontend
Please, help me understand what I'm missing. Many Thanks!
There are several locations that needed to be fixed
In fetchData.tsx
export function useGetData(urlpath: string) {
const [droneData, setData] = useState<any>([]);
async function handleDataFetch(path: string) {
const result = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
...
});
const response = await result.json();
setData(response);
}
useEffect(() => {
handleDataFetch(urlpath);
}, []);
Explanation:
you need a "blank" array for looping through. I guess that the error causes by the fact that at the start, before the data is fetched, there is nothing to loop through. It's same as doing undefined.map(), which is obviously fail.
You need a dependencies array for useEffect. Right now your code will do an infinite loop since everytime it get data, it update the state, thus re-run the useEffect and repeat. Add dependencies array limit when that useEffect will run
In Drones.tsx
return (
<div>
{data.map(item => <div>{item.name}</div>}
</div>
)
Not much to say here. I don't use Angular so I'm not sure why you use Array.prototype.map, but in React you can loop through your variable directly. I also have a CodeSandbox link for your project (I use public API)

Event-driven approach in React?

I'd like to "fire an event" in one component, and let other components "subscribe" to that event and do some work in React.
For example, here is a typical React project.
I have a model, fetch data from server and several components are rendered with that data.
interface Model {
id: number;
value: number;
}
const [data, setData] = useState<Model[]>([]);
useEffect(() => {
fetchDataFromServer().then((resp) => setData(resp.data));
}, []);
<Root>
<TopTab>
<Text>Model with large value count: {data.filter(m => m.value > 5).length}</Text>
</TobTab>
<Content>
<View>
{data.map(itemData: model, index: number) => (
<Item key={itemData.id} itemData={itemData} />
)}
</View>
</Content>
<BottomTab data={data} />
</Root>
In one child component, a model can be edited and saved.
const [editItem, setEditItem] = useState<Model|null>(null);
<Root>
<TopTab>
<Text>Model with large value count: {data.filter(m => m.value > 5).length}</Text>
</TobTab>
<ListScreen>
{data.map(itemData: model, index: number) => (
<Item
key={itemData.id}
itemData={itemData}
onClick={() => setEditItem(itemData)}
/>
)}
</ListScreen>
{!!editItem && (
<EditScreen itemData={editItem} />
)}
<BottomTab data={data} />
</Root>
Let's assume it's EditScreen:
const [model, setModel] = useState(props.itemData);
<Input
value={model.value}
onChange={(value) => setModel({...model, Number(value)})}
/>
<Button
onClick={() => {
callSaveApi(model).then((resp) => {
setModel(resp.data);
// let other components know that this model is updated
})
}}
/>
App must let TopTab, BottomTab and ListScreen component to update data
without calling API from server again (because EditScreen.updateData already fetched updated data from server) and
not passing updateData function as props (because in most real cases, components structure is too complex to pass all functions as props)
In order to solve above problem effectively, I'd like to fire an event (e.g. "model-update") with an argument (changed model) and let other components subscribe to that event and change their data, e.g.:
// in EditScreen
updateData().then(resp => {
const newModel = resp.data;
setModel(newModel);
Event.emit("model-updated", newModel);
});
// in any other components
useEffect(() => {
// subscribe model change event
Event.on("model-updated", (newModel) => {
doSomething(newModel);
});
// unsubscribe events on destroy
return () => {
Event.off("model-updated");
}
}, []);
// in another component
useEffect(() => {
// subscribe model change event
Event.on("model-updated", (newModel) => {
doSomethingDifferent(newModel);
});
// unsubscribe events on destroy
return () => {
Event.off("model-updated");
}
}, []);
Is it possible using React hooks?
How to implement event-driven approach in React hooks?
There cannot be an alternative of event emitter because React hooks and use context is dependent on dom tree depth and have limited scope.
Is using EventEmitter with React (or React Native) considered to be a good practice?
A: Yes it is a good to approach when there is component deep in dom tree
I'm seeking event-driven approach in React. I'm happy with my solution now but can I achieve the same thing with React hooks?
A: If you are referring to component state, then hooks will not help you share it between components. Component state is local to the component. If your state lives in context, then useContext hook would be helpful.
For useContext we have to implement full context API with MyContext.Provider and MyContext.Consumer and have to wrap inside high order (HOC) component
Ref
so event emitter is best.
In react native, you can use react-native-event-listeners package
yarn add react-native-event-listeners
SENDER COMPONENT
import { EventRegister } from 'react-native-event-listeners'
const Sender = (props) => (
<TouchableHighlight
onPress={() => {
EventRegister.emit('myCustomEvent', 'it works!!!')
})
><Text>Send Event</Text></TouchableHighlight>
)
RECEIVER COMPONENT
class Receiver extends PureComponent {
constructor(props) {
super(props)
this.state = {
data: 'no data',
}
}
componentWillMount() {
this.listener = EventRegister.addEventListener('myCustomEvent', (data) => {
this.setState({
data,
})
})
}
componentWillUnmount() {
EventRegister.removeEventListener(this.listener)
}
render() {
return <Text>{this.state.data}</Text>
}
}
Not sure why the EventEmitter has been downvoted, but here's my take:
When it comes to state management, I believe using a Flux-based approach is usually the way to go (Context/Redux and friends are all great). That said, I really don't see why an event-based approach would pose any problem - JS is event based and React is just a library after all, not even a framework, and I can't see why we would be forced to stay within its guidelines.
If your UI needs to know about the general state of your app and react to it, use reducers, update your store, then use Context/Redux/Flux/whatever - if you simply need to react to specific events, use an EventEmitter.
Using an EventEmitter will allow you to communicate between React and other libraries, e.g. a canvas (if you're not using React Three Fiber, I dare you to try and talk with ThreeJS/WebGL without events) without all the boilerplate. There are many cases where using Context is a nightmare, and we shouldn't feel restricted by React's API.
If it works for you, and it's scalable, just do it.
EDIT: here's an example using eventemitter3:
./emitter.ts
import EventEmitter from 'eventemitter3';
const eventEmitter = new EventEmitter();
const Emitter = {
on: (event, fn) => eventEmitter.on(event, fn),
once: (event, fn) => eventEmitter.once(event, fn),
off: (event, fn) => eventEmitter.off(event, fn),
emit: (event, payload) => eventEmitter.emit(event, payload)
}
Object.freeze(Emitter);
export default Emitter;
./some-component.ts
import Emitter from '.emitter';
export const SomeComponent = () => {
useEffect(() => {
// you can also use `.once()` to only trigger it ... once
Emitter.on('SOME_EVENT', () => do what you want here)
return () => {
Emitter.off('SOME_EVENT')
}
})
}
From there you trigger events wherever you want, subscribe to them, and act on it, pass some data around, do whatever you want really.
We had a similar problem and took inspiration from useSWR.
Here is a simplified version of what we implemented:
const events = [];
const callbacks = {};
function useForceUpdate() {
const [, setState] = useState(null);
return useCallback(() => setState({}), []);
}
function useEvents() {
const forceUpdate = useForceUpdate();
const runCallbacks = (callbackList, data) => {
if (callbackList) {
callbackList.forEach(cb => cb(data));
forceUpdate();
}
}
const dispatch = (event, data) => {
events.push({ event, data, created: Date.now() });
runCallbacks(callbacks[event], data);
}
const on = (event, cb) => {
if (callbacks[event]) {
callbacks[event].push(cb);
} else {
callbacks[event] = [cb];
}
// Return a cleanup function to unbind event
return () => callbacks[event] = callbacks[event].filter(i => i !== cb);
}
return { dispatch, on, events };
}
In a component we do:
const { dispatch, on, events } = useEvents();
useEffect(() => on('MyEvent', (data) => { ...do something...}));
This works nicely for a few reasons:
Unlike the window Event system, event data can be any kind of object. This saves having to stringify payloads and what not. It also means there is no chance of collision with any built-in browser events
The global cache (idea borrowed from SWR) means we can just useEvents wherever needed without having to pass the event list & dispatch/subscribe functions down component trees, or mess around with react context.
It is trivial to save the events to local storage, or replay/rewind them
The one headache we have is the use of the forceUpdate every time an event is dispatched means every component receiving the event list is re-rendered, even if they are not subscribed to that particular event. This is an issue in complex views.
We are actively looking for solutions to this...
You can create use context in App.js using useContext, and then in you child component you can use values from it and update the context as soon as the context get updated it will update the values being used in other child component, no need to pass props.
You can achieve this with any React's global state management.
In your store, have a useEffect for your event subscription, and a reducer for each of your event.
If you have 2 data sources, the subscription and the query, then initialize your state values with your query, then listen to the subscription.
Something like this
const reducer = (state, action) => {
switch(action.type) {
case 'SUBSCRIBE':
return action.payload
default:
return state
}
}
Assuming you are using https://github.com/dai-shi/use-reducer-async
const asyncActions = {
QUERY: ({ dispatch }) => async(action) => {
const data = await fetch(...)
dispatch({ type: 'query', payload: data })
}
}
You can also use middleware in Redux
const [state, dispatch] = useReducer(reducer, initialValues, asyncActions)
useEffect(() => {
dispatch({ type: 'QUERY' })
Event.on((data) => {
dispatch({ type: 'SUBSCRIBE', payload: data })
})
return () => Event.off()
}, [])
return <Provider value={state}>{children}</Provider>

React and firebase adding document to collection issue - Warning: Can't perform a React state update on an unmounted component

I'm trying to add a document to a firebase collection. It works and adds the document but i get the following warning in my react console -
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.
This is the utility function I wrote
export const addDocument = async (title) => {
const collectionRef = firestore.collection('collections')
const newDocRef = collectionRef.doc()
const snapshot = await newDocRef.get()
if(!snapshot.exists) {
try {
await newDocRef.set({
title,
items: []
})
} catch (e) {
console.error('Error creating document.', e.message)
}
}
}
This is the component that i'm calling the utility in and is giving me the error
const AdminPage = () => {
const handleSubmit = () => {
addDocument('hat')
}
return (
<div>
<button onClick={handleSubmit}>Add Collection</button>
</div>
)
}
All my imports and exports are fine i've just omitted it to make things more concise.
The code contains async operations so you should implement method: componentWillUnmount
As your code awaits for all async operation (get and set only I think) it should not be a case in your example. This is just a warning and according to my understanding it is showing that async operation may end when object is already destroyed.
Passing some interesting articles and questions:
one, two, three, four
if you need more there is tones of articles over the internet.
I hope it will help!

Redux useSelector not updated, need to be refresh

i face this issue.
when i have a Axios call, which promise to dispatch a action to update the redux and execute the callback.
but when callback is executed, the redux state seem to be stale.
i got a sandbox code for demo here
if you click on the getNewDate Button, the console will show the difference in the redux state.
the state will be correct when redux cause a re-render.
How do i get the correct redux state during callback?
The response will always be stale, that's how React hooks work. They apply a closure over all the variables in each individual render when they are created. If you absolutely need the value to not be stale in a callback function (or effect), set up a ref for it.
const { response, getNewDate } = useResponse();
const responseRef = useRef(response);
useLayoutEffect(() => {
responseRef.current = response;
}, [response]);
const callbackSuccessful = (data: IResponse) => {
console.log("response is not Stale: " + responseRef.current.newDate);
console.log("should be: " + data.newDate);
};
Once you set up a ref, you'll clearly see that the response is in-fact changing and the responseRef.current shows the same value as data.newDate.
You have to useLayoutEffect here because the order in which the effect runs is wrong for the callback. Since useEffect runs after the component re-renders while useLayoutEffect runs while the component re-renders.
Another way you could see that the useSelector is working fine and updating and that your MyPages.tsx is seeing that update is useEffect to log the change whenever it changes.
useEffect(() => {
console.log(response.newDate)
}, [response]);
If you want access to the latest redux store state in a callback without any timing issues at all, useStore is helpful, and it doesn't cause re-rendering at all.
const store = useStore();
const callbackSuccessful = (data: IResponse) => {
console.log("should be: " + data.newDate);
console.log("Redux store: " + store.getState().apiResponse.newDate);
};
https://codesandbox.io/s/react-typescript-demo-pek65?file=/src/pages/MyPages.tsx
The use previous answer of Zachary Haber's useLayoutEffect is the correct answer.
But here are two subpar solutions that I can share, both with their own issues.
Solution 1
Use a key on the button to inform React that it should renew the scope of the button object (i.e. re-mount the component) because some of it's dependencies have updated.
import React from "react";
import { useResponse } from "../hooks/useResponse";
import { IResponse } from "../utils/apiDef";
const MyPages = () => {
const { response, getNewDate } = useResponse();
const callbackSuccessful = (data: IResponse) => {
console.log("response is: " + response.newDate)
console.log("callback data is: " + data.newDate)
}
const callbackFail = (data: any) => {
}
const handleButton = () => {
getNewDate(callbackSuccessful, callbackFail)
}
const buttonKey = response.newDate
return <div>
hello world {response.newDate}
<br/>
<button key={buttonKey} type="button" onClick={handleButton}>getNewDate</button>
</div>;
};
export default MyPages;
A slight problem: This will display the previous result, i.e. response.newDate will contain the value it had when the key was changed.
Solution 2
Use a global variable e.g. state
import React from "react";
import { useResponse } from "../hooks/useResponse";
import { IResponse } from "../utils/apiDef";
const state = {
response: undefined
}
const MyPages = () => {
const { response, getNewDate } = useResponse();
state.response = response;
const callbackSuccessful = (data: IResponse) => {
console.log("state response is: " + state.response.newDate)
console.log("callback data is: " + data.newDate)
}
const callbackFail = (data: any) => {
}
const handleButton = () => {
getNewDate(callbackSuccessful, callbackFail)
}
return <div>
hello world {response.newDate}
<br/>
<button type="button" onClick={handleButton}>getNewDate</button>
</div>;
};
export default MyPages;
In the call back the state.response.newDate is equal to data.newDate.
A slight problem: It's not a reusable component anymore. This solution will only work if you ever have one MyPages object in your app. All instances will point to the same static global variable and this will introduce a contention of whoever writes last wins.
You should not do:
<MyPages />
<MyPages />
Another issue I have with this solution: React's optimization; I don't know how this will affect it.
I hope this helps.

Best practice to prevent state update warning for unmounted component from a handler

It is a common use-case to fetch and display the data from an external API (by using XHR requests) when a certain UI component (e.g. a <button />) is clicked. However, if the component was unmounted in the meantime, the following warning appears in the console:
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 a useEffect cleanup function.
In fact, the most common solution (approved by #dan-abramov) to avoid the warning seems to keep track of the mount state of the component by using the return function of useEffect to cleanup.
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
export default function PhotoList() {
const mounted = useRef(true);
const [photos, setPhotos] = useState(null);
useEffect(() => {
return () => {
mounted.current = false;
};
}, []);
function handleLoadPhotos() {
axios("https://jsonplaceholder.typicode.com/photos").then(res => {
if (mounted.current) {
setPhotos(res.data);
}
});
}
return (
<div>
<button onClick={handleLoadPhotos}>Load photos</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
However, this seems to cause unnecessary overhead to keep track of the mounting state and to check it before every state update. This becomes especially obvious when Observables (where you can unsubscribe) instead of Promises are used.
While you indeed can unsubscribe inside of the useEffect using the cleanup function in a very neat way:
useEffect(() => {
// getPhotos() returns an observable of the photo list
const photos$ = getPhotos().subscribe(setPhotos);
return () => photos$.unsubscribe();
}, []);
The same smart cleanup is not possible within a handler:
function handleLoadPhotos() {
const photos$ = getPhotos().subscribe(setPhotos);
// how to unsubscribe on unmounting?
}
Is there a best practice to avoid the warning without the ugly manual tracking of the mounting state with useRef()? Are there good approaches for that when using Observables?
Problem is that you are trying to fetch data in your component. This is not a good idea since the component could be unmounted and you would face many possible errors.
So that, you should look for other ways.
I always do async operations in redux thunks.
You should avoid your approach. Use redux and redux-thunk if you like. If not, try to find another solution to move async operations outside of your components.
In fact, you should be writing declarative ui components which renders for given props. So that, your data should be outside of your components logic too.
That's an awesome question! This is how I would do it:
First, define a helper function (it's not cheating because it really is a highly reusable function whenever you're dealing with React and observables combined):
import * as React from 'react';
import { Observable } from 'rxjs';
export const useObservable = <Value>(
arg: () => {
observable: Observable<Value>;
value: Value;
},
) => {
const { observable, value } = React.useMemo(arg, []);
const [state, setState] = React.useState<Value>(value);
React.useEffect(() => {
const subscription = observable.subscribe(value => setState(value));
return () => subscription.unsubscribe();
}, []);
return state;
};
Just to help illustrate what this function does, the following component will display the latest value emitted by myObservable:
() => {
const value = useObservable(() => ({
observable: myObservable,
value: 'Nothing emitted yet',
}));
return <span>{value}</span>;
};
Your component will then look like this:
export default function PhotoList() {
const clicksSubject = React.useMemo(() => new Subject<undefined>(), []);
const photos = useObservable(() => ({
observable: clicksSubject.pipe(
switchMap(() => axiosApiCallReturningAnObservable()),
map(res => res.data),
),
value: null,
}));
return (
<div>
<button
onClick={() => {
clicksSubject.next(undefined);
}}
>
Load photos
</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
When the component is dismounted, useObservable unsubs from the observable that was passed to it. This makes sure that we don't at a later point attempt to set the state, and that the data fetching API aborts (or at least gets a chance to abort) the HTTP request.

Resources