Third party API needs to access state in React app - javascript

We have a React app which communicates with a third party library for phone integration. Whenever someone calls, the third-party library triggers a callback function inside the React app. That has been fine until now, but now this callback function needs to access the current state which seems to pose a problem. The state inside of this callback function, seems to always be at the initial value and never updates.
I have made a small sandbox here to describe the problem: https://codesandbox.io/s/vigorous-panini-0kge6?file=/src/App.js
In the sandbox, the counter value is updated correctly when I click "Internal increase". However, the same function has been added as a callback to ThirdPartyApi, which is called when I click "External increase". When I do that, the counter value reverts to whatever is the default in useState.
How can I make the third library be aware of state updates from inside React?
App.js:
import React, { useState, useEffect } from "react";
import ThirdPartyApi from "./third-party-api";
import "./styles.css";
let api = new ThirdPartyApi();
export default function App() {
const [counter, setCounter] = useState(5);
const increaseCounter = () => {
setCounter(counter + 1);
console.log(counter);
};
useEffect(() => {
api.registerCallback(increaseCounter);
}, []);
return (
<div className="App">
<p>
<button onClick={() => increaseCounter()}>Internal increase</button>
</p>
<p>
<button onClick={() => api.triggerCallback()}>External increase</button>
</p>
</div>
);
}
third-party-api.js:
export default class ThirdPartyApi {
registerCallback(callback) {
this.callback = callback;
}
triggerCallback() {
this.callback();
}
}

You need to wrap increaseCounter() into a callback via React's useCallback.
As it is, api.registerCallback() rerenders because of it, resetting counter.
You can learn more about this behavior here.
import React, { useState, useCallback, useEffect } from "react";
import ReactDOM from "react-dom";
class ThirdPartyApi {
registerCallback(callback) {
this.callback = callback;
}
triggerCallback() {
this.callback();
}
}
let api = new ThirdPartyApi();
function App() {
const [counter, setCounter] = useState(5);
const increaseCounter = useCallback(() => {
setCounter(counter + 1);
console.log(counter);
}, [counter]);
useEffect(() => {
api.registerCallback(increaseCounter);
}, [increaseCounter]);
return (
<div className="App">
<p>
<button onClick={() => increaseCounter()}>Internal increase</button>
</p>
<p>
<button onClick={() => api.triggerCallback()}>External increase</button>
</p>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);

Related

useState in onclick handler

Here's the UI
Where I click the first button, then click the second button, it shows value 1, but I expect it to show value 2, since I set the value to 2. What's the problem and how should I address that?
Here's the code:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import React, {
useState,
useEffect,
useMemo,
useRef,
useCallback
} from "react";
const App = () => {
const [channel, setChannel] = useState(null);
const handleClick = useCallback(() => {
console.log(channel);
}, [channel]);
const parentClick = () => {
console.log("parent is call");
setChannel(2);
};
useEffect(() => {
setChannel(1);
});
return (
<div className="App">
<button onClick={parentClick}>Click to SetChannel 2</button>
<button onClick={handleClick}>Click to ShowChannel 2</button>
</div>
);
};
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(<App />);
Here's the codesandbox
Add a dependency to the useEffect hook, if you don't add any dependency it will just re-run on every state change.
Change this:
useEffect(() => {
setChannel(1);
});
To this:
useEffect(() => {
setChannel(1);
}, []);
useEffect(() => {
setChannel(1);
});
Runs on every render, so it's always reverting back to 1
Your problem is that you are setting channel value to 1 every render.
You have 2 options.
Set initial state value of channel to 1 (see below)
Call this.setState({channel: 1}) in the componentDidMount method.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {channel: 1};
}
handleClick=(evt)=> {
console.log(this.state.channel);
}
parentClick=(evt)=> {
this.setState({channel: 2});
}
render() {
return (
<div className="App">
<button onClick={this.parentClick}>Click to SetChannel 2</button>
<br /><br />
<button onClick={this.handleClick}>Click to ShowChannel 2</button>
</div>
);
}
}
PS: It's not clear what you're trying to do & your sandbox is quite different from the code you have posted here.

React (Next) won't re-render after redux state change (yes, state returned as new object)

I am facing a problem with re-rendering after a state change in my NextJS app.
The function sendMessageForm launches a redux action sendMessage which adds the message to the state.
The problem is unrelated to the returned state in the reducer as I am returning a new object(return {...state}) which should trigger the re-render!
Is there anything that might block the re-render ?
This is the file that calls & displays the state, so no other file should be responsible ! But if you believe the problem might lie somewhere else, please do mention !
import { AttachFile, InsertEmoticon, Mic, MoreVert } from '#mui/icons-material';
import { Avatar, CircularProgress, IconButton } from '#mui/material';
import InfiniteScroll from 'react-infinite-scroller';
import Head from 'next/head';
import { useState, useEffect } from 'react';
import Message from '../../components/Message.component';
import styles from '../../styles/Chat.module.css'
import { useRouter } from 'next/router'
import {useSelector, useDispatch} from "react-redux"
import {bindActionCreators} from "redux"
import * as chatActions from "../../state/action-creators/chatActions"
const Chat = () => {
const router = useRouter()
const { roomId } = router.query
const auth = useSelector((state)=> state.auth)
const messages = useSelector((state)=> state.chat[roomId].messages)
const dispatch = useDispatch()
const {getMessages, markAsRead, sendMessage} = bindActionCreators(chatActions, dispatch)
const [inputValue, setInputValue] = useState("")
const sendMessageForm = (e) => {
e.preventDefault()
console.log("***inputValue:", inputValue)
sendMessage(roomId, inputValue)
}
const loadMessages = (page) => {
if(roomId)
getMessages(roomId, page)
}
//user-read-message
useEffect(() => {
//user-read-message
markAsRead(roomId, auth.user._id)
}, [messages]);
return (
<div className={styles.container}>
<Head>
<title>Chat</title>
</Head>
<div className={styles.header}>
<Avatar/>
<div className={styles.headerInformation}>
<h3>Zabre el Ayr</h3>
<p>Last Seen ...</p>
</div>
<div className={styles.headerIcons}>
<IconButton>
<AttachFile/>
</IconButton>
<IconButton>
<MoreVert/>
</IconButton>
</div>
</div>
<div className={styles.chatContainer}>
<InfiniteScroll
isReverse={true}
pageStart={0}
loadMore={loadMessages}
hasMore={messages.hasNextPage || false}
loader={<div className={styles.loader} key={0}><CircularProgress /></div>}
>
{Object.keys(messages.docs).map((key, index)=>{
return<Message
key={index}
sentByMe={messages.docs[key].createdBy === auth.user._id}
message={messages.docs[key].msg}
/>})}
</InfiniteScroll>
<span className={styles.chatContainerEnd}></span>
</div>
<form className={styles.inputContainer}>
<InsertEmoticon/>
<input className={styles.chatInput} value={inputValue} onChange={(e)=>setInputValue(e.target.value)}/>
<button hidden disabled={!inputValue} type='submit' onClick={sendMessageForm}></button>
<Mic/>
</form>
</div>)
};
export default Chat;
useSelector requires a new object with a new reference from the object you are passing to it in order to trigger the re-render
What you're doing with return {...state} is just creating a new object for the parent object but not the nested one useSelector is using, which is in your case :
const messages = useSelector((state)=> state.chat[roomId].messages)
So, you should return the whole state as a new object WITH a new state.chat[roomId].messages object
In other words, the references for the root object & the one being used should be changed.

React search using debounce

I am trying to implement a search that makes a new query on each character change. After n milliseconds, I need to make a change to the object that stores some properties.
//user typing
const onInputChange = (e) => {
let searchInput = e.target.value;
useDebounce(
handleSearchPropsChange({
filter: {
searchInput,
dateRange: {
start,
end
}
}
}), 1000
);
}
The function I am using for the delayed call
import {debounce} from 'lodash';
import {useRef} from 'react';
export function useDebounce(callback = () => {}, time = 500) {
return useRef(debounce(callback, time)).current;
}
But I am getting the error:
Invalid hook call. Hooks can only be called inside of the body of a function component. This
could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
A example without lodash, just Hooks.
UseDebounce.js
import { useEffect, useCallback } from 'react';
export default function useDebounce(effect, dependencies, delay) {
const callback = useCallback(effect, dependencies);
useEffect(() => {
const timeout = setTimeout(callback, delay);
return () => clearTimeout(timeout);
}, [callback, delay]);
}
App.js
import React, { useState } from 'react';
import useDebounce from './useDebounce';
import data from './data';
export default function App() {
const [search, setSearch] = useState('');
const [filteredTitle, setFilteredTitle] = useState([]);
// DeBounce Function
useDebounce(() => {
setFilteredTitle(
data.filter((d) => d.title.toLowerCase().includes(search.toLowerCase()))
);
}, [data, search], 800
);
const handleSearch = (e) => setSearch(e.target.value);
return (
<>
<input
id="search"
type="text"
spellCheck="false"
placeholder="Search a Title"
value={search || ''}
onChange={handleSearch}
/>
<div>
{filteredTitle.map((f) => (
<p key={f.id}>{f.title}</p>
))}
</div>
</>
);
}
Demo : Stackblitz

How to cleanup setTimeout/setInterval in event handler in React?

How can I clean up function like setTimeout or setInterval in event handler in React? Or is this unnecessary to do so?
import React from 'react'
function App(){
return (
<button onClick={() => {
setTimeout(() => {
console.log('you have clicked me')
//How to clean this up?
}, 500)
}}>Click me</button>
)
}
export default App
Whether it's necessary depends on what the callback does, but certainly if the component is unmounted it almost doesn't matter what it does, you do need to cancel the timer / clear the interval.
To do that in a function component like yours, you use a useEffect cleanup function with an empty dependency array. You probably want to store the timer handle in a ref.
(FWIW, I'd also define the function outside of the onClick attribute, just for clarity.)
import React, {useEffect, useRef} from 'react';
function App() {
const instance = useRef({timer: 0});
useEffect(() => {
// What you return is the cleanup function
return () => {
clearTimeout(instance.current.timer);
};
}, []);
const onClick = () => {
// Clear any previous one (it's fine if it's `0`,
// `clearTimeout` won't do anything)
clearTimeout(instance.current.timer);
// Set the timeout and remember the value on the object
instance.current.timer = setTimeout(() => {
console.log('you have clicked me')
//How to clean this up?
}, 500);
};
return (
<button onClick={onClick}>Click me</button>
)
}
export default App;
An object you store as a ref is usually a useful place to put things you would otherwise have put on this in a class component.
(If you want to avoid re-rendering button when other state in your component changes (right now there's no other state, so no need), you could use useCallback for onClick so button always sees the same function.)
One more solution (Live Demo):
import React, { useState } from "react";
import { useAsyncCallback } from "use-async-effect2";
import { CPromise } from "c-promise2";
export default function TestComponent(props) {
const [text, setText] = useState("");
const click = useAsyncCallback(function* (ms) {
yield CPromise.delay(ms);
setText("done!" + new Date().toLocaleTimeString());
}, []);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={() => click(2000)}>Click me!</button>
<button onClick={click.cancel}>Cancel scheduled task</button>
</div>
);
}
In case if you want to cancel the previous pending task (Live demo):
import React, { useState } from "react";
import { useAsyncCallback } from "use-async-effect2";
import { CPromise } from "c-promise2";
export default function TestComponent(props) {
const [text, setText] = useState("");
const click = useAsyncCallback(
function* (ms) {
console.log("click");
yield CPromise.delay(ms);
setText("done!" + new Date().toLocaleTimeString());
},
{ deps: [], cancelPrevios: true }
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={() => click(5000)}>Click me!</button>
<button onClick={click.cancel}>Cancel scheduled task</button>
</div>
);
}
Clear timer when unmount component
import React from 'react'
function App(){
const timerRef = React.useRef(null)
React.useEffect(() => {
return () => {
// clean
timerRef.target && clearTimeout(timerRef.target)
}
},[])
return (
<button onClick={() => {
timerRef.target = setTimeout(() => {
console.log('you have clicked me')
}, 500)
}}>Click me</button>
)
}
export default App

Too many re-renders React error while fetching data from API

I am building a simple recipe app and I have a problem with fetching my data from the API, because the code seems to run on every render and I do not even understand why it re-runs since I found that if I add the dependency array, it should run only once, right ?
App.js
function App() {
const [recipesList, setRecipesList] = useState([]);
let [scroll, setScroll] = useState(0)
console.log(recipesList,"list");
return (
<div className="App">
<img className="logo" src={logo} alt="Logo"/>
<Recipes recipesList={recipesList} getRecipes={setRecipesList} />
</div>
);
}
export default App;
Recipes.js
import React, {useEffect, useState} from "react";
import Recipe from "../Recipe/Recipe";
import "./Recipes.css";
const Recipes = (props) => {
useEffect( () => {
if (props.recipesList.length === 0) {
fetch("myapi.com/blablabla")
.then(res => res.json())
.then(result => {
props.getRecipes(result.recipes);
}
)
}
else {
console.log("Do not fetch");
}
return () => console.log("unmounting");
}, [props])
const recipeComponent = props.recipesList.map( (item) => {
return <Recipe className="recipe" info={item}/>
})
return(
<div className="recipes">
{recipeComponent}
<h1>Hello</h1>
</div>
)
}
export default Recipes;
Components will re-render every time your the props or state changes inside of the component.
I would recommend keeping the fetching logic inside of the Recipes component, because A: its recipe related data, not app related data. And B: this way you can control the state in Recipes instead of the props. This will give you more control on how the component behaves instead of being dependent on the parent component.
In the useEffect hook, leave the dependency array empty. This will cause the component to render, call useEffect only the first time, load your data and then render the recipes without re-rendering further.
import React, { useEffect, useState } from "react";
import Recipe from "../Recipe/Recipe";
import "./Recipes.css";
const Recipes = () => {
const [recipesList, setRecipesList] = useState([]);
useEffect(() => {
fetch("myapi.com/blablabla")
.then((res) => res.json())
.then((result) => {
setRecipesList(result.recipes);
});
return () => console.log("unmounting");
}, []);
// On the first render recipeComponents will be empty.
const recipeComponents = recipesList.map((item) => <Recipe className="recipe" info={item}/>)
return (
<div className="recipes">
{recipeComponents}
<h1>Hello</h1>
</div>
);
};
export default Recipes;
try this code :
function App() {
const [recipesList, setRecipesList] = useState([]);
let [scroll, setScroll] = useState(0)
const getListPropd = (e) => {
setRecipesList(e)
}
console.log(recipesList,"list");
return (
<div className="App">
<img className="logo" src={logo} alt="Logo"/>
<Recipes recipesList={(e) => getListPropd (e)} getRecipes={setRecipesList} />
</div>
);
}
export default App;
const [checkData , setCheckData ] = useState(true)
useEffect( () => {
if (checkData) {
fetch("myapi.com/blablabla")
.then(res => res.json())
.then(result => {
props.recipesList(result.recipes);
}
if(props.recipesList.length > 0) {
setCheckData(false)
}
)
else {
console.log("Do not fetch");
}
return () => console.log("unmounting");
}, [checkData])
the useEffect hook uses an empty dependency array, [] if it should ONLY run once after component is mounted. This is the equivalent of the old lifecycle method componentDidMount()
If you add a non-empty dependency array, then the component rerenders EVERY time this changes. In this case, every time your component receives new props (i.e. from a parent component, this triggers a reload.
see more info here https://reactjs.org/docs/hooks-effect.html , especially the yellow block at the bottom of the page
Happy coding!

Categories