Trigger useEffect from functional Component with global variable - javascript

my question is if it is possible to trigger useEffect with a variable from outside the component.
In my case i have this main component that has the useEffect responsible to update info every time the variable "refresh" changes.
function Main() {
const [data, setData] = useState([])
const [refresh, setRefresh] = useState(false)
useEffect(async () => {
await axios.get(`${process.env.REACT_APP_SERVER_URL}/api/data/`)
.then(res => {
setData(res.data);
})
}, [refresh]);
And then i have a function that i can invoke inside the component or child that triggers the useEffect, updating the data.
const refreshData = () => setRefresh(!refresh);
So far so good, it works as i wanted but now i needed to export this function to use in a component not related to this one, but i know that you cannot export a function declared inside a component.
So my idea was to create this same function outside the component, like so:
let refreshOutside = false;
export const refreshMainFromOutside = () => {
refreshOutside = !refreshOutside;
}
So now i can add the variable "refreshOutside" to the useEffect, like so:
useEffect(async () => {
await axios.get(`${process.env.REACT_APP_SERVER_URL}/api/data/`)
.then(res => {
setData(res.data);
})
}, [refresh, refreshOutside]);
But if i import it in other component and invoke the method it does not trigger the useEffect, i am kinda new to react but i think its because the component is not re-rendering.
Is there any solution that might work on my case?
Thanks in advance.

I suggest you put your hooks inside another file for example useComponent.js and export your refreshData as const inside it, then use that hook inside any component you wish:
const useComponent = () => {
const [data, setData] = useState([])
const [refresh, setRefresh] = useState(false)
useEffect(async () => {
await axios.get(`${process.env.REACT_APP_SERVER_URL}/api/data/`)
.then(res => {
setData(res.data);
})
}, [refresh, refreshOutside]);
const refreshData = () => setRefresh(!refresh);
export { refreshData }
}
export default useComponent
import the hook inside any component then destructure functions and use them:
import useComponent from '../hooks/useComponent'
const MyComponent = () => {
const { refreshData } = useComponent()
return <button onClick={refreshData}>Click to refresh!</button>
}
export default MyComponent

As mentioned in the comment, you can simply define a separate function to fetch the data (and memoize it with the useCallback() hook), then you can use that function wherever you want in your Main component and in any Child component to whom you pass it as prop.
Maybe an example would make it easier to understand:
const Main = () => {
const [data, setData] = React.useState([]);
const updateData = React.useCallback((startIndex) => {
/*
axios.get(`${process.env.REACT_APP_SERVER_URL}/api/data/`)
.then(result => {
setData(result.data);
})
*/
axios.get('https://jsonplaceholder.typicode.com/posts/')
.then((result) => {
console.log('fetching data...');
setData(result.data.slice(startIndex, startIndex + 5));
console.log('... data updated');
});
}, [setData]);
React.useEffect(() => {
updateData(0); // fetch & update `data` when the Main component mounts
}, [])
return (
<div>
<h1>Main</h1>
{
data.length > 0
? <ul>{data.map(item => <li>{item.title}</li>)}</ul>
: <p>'There are no data'</p>
}
<button
onClick={() => updateData(5)} // fetch & update `data` on request, on button click
>Refresh from Main</button>
<Child handleClick={updateData} />
</div>
)
}
const Child = ({handleClick}) => {
return (
<React.Fragment>
<h1>Child</h1>
<button
onClick={() => handleClick(10)} // fetch & update `data` on request, on button click
>Refresh from Child</button>
</React.Fragment>
)
}
ReactDOM.render(<Main />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.js"></script>
<script crossorigin src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<div id="app"></div>
P.S. in the above example I used a parameter (startIndex) for the updateData() function just to keep the data state short and make it change when different buttons are clicked (because the API I used always returns the same data). In a real case use you are unlikely to do something like that because you can implement pagination on API side (if needed) and your API response is supposed to change over time (or you would not need a way to refresh your data).
Anyway, the point is that what I did inside the updateData() function body is mostly irrelevant; the main take away of the snippet is supposed to be how you can handle a function that needs to be called inside hooks, inside the main component and by child components.

Related

Infinite console log in react js component

I have made two simple straight forward component is React, used a open source API to test API integration. React is showing this weird behavior of infinite console logs. I don't understand the issue. I'm using the fetch function for making API calls and functional component.
App component:
function App() {
const [characters, setCharac] = useState([])
const URL = "https://swapi.dev/api/";
fetch(URL + "people").then(response => response.json().then(data => {
setCharac(data.results)
console.log('Test');
}))
return (
<div className="App">
{characters.map(charac => {
return <Character {...charac} />
})}
</div>
);
}
Character component:
const Character = (props) => {
console.log(props);
return (
<div key={props.name}>
<h1>{props.name}</h1>
<p>{props.height}</p>
</div>
);
}
console.log('Test'); in App component and console.log(props); in Character component are being executed infinitely.
This is the render method
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Your components are rendering multiple times because your state is changed every time you fetch data (because of setState).
Try creating a function fetchData(). Make this function async as well to wait for data to be retrieved.
const fetchData = async () => {
const result = await fetch(URL + "people").then(response => response.json().then(data => {
setCharac(data.results)
console.log('Test');
return data;
}));
return result;
}
and then use it inside useEffect (Read more about useEffects: React hooks: What/Why `useEffect`?)
useEffect(() => {
fetchData();
}, []);
Note the usage of [] in useEffect. The data will be fetched only once when you load the component.
Try wrapping it in a useEffect
e.g.
useEffect(()=>{
fetch(URL + "people").then(response => response.json().then(data => {
setCharac(data.results)
console.log('Test');
}))
},[])
otherwise every time the state is set it is firing off the fetch again because a re-render is being triggered.
Because you fetch some data, update the state, which causes a re-render, which does another fetch, updates the state, which causes another render...etc.
Call your fetch function from inside a useEffect with an empty dependency array so that it only gets called once when the component is initially rendered.
Note 1: you can't immediately log the state after setting it as setting the state is an async process. You can, however, use another useEffect to watch for changes in the state, and log its updated value.
Note 2: I've used async/await in this example as the syntax is a little cleaner.
// Fetch the data and set the state
async function getData(endpoint) {
const json = await fetch(`${endpoint}/people`);
const data = await response.json();
setCharac(data.results);
}
// Call `getData` when the component initially renders
useEffect(() => {
const endpoint = 'https://swapi.dev/api';
getData(endpoint);
}, []);
// Watch for a change in the character state, and log it
useEffect(() => console.log(characters), [characters]);
You can do something like this:
import React, { useState, useEffect, useCallback } from "react";
const Character = (props) => {
console.log(props);
return (
<div key={props.name}>
<h1>{props.name}</h1>
<p>{props.height}</p>
</div>
);
};
export default function App() {
const [characters, setCharac] = useState([]);
const makeFetch = useCallback(() => {
const URL = "https://swapi.dev/api/";
fetch(URL + "people").then((response) =>
response.json().then((data) => {
setCharac(data.results);
console.log("Test");
})
);
}, []);
useEffect(() => {
makeFetch();
}, []);
return (
<div className="App">
{characters.map((charac) => {
return <Character {...charac} />;
})}
</div>
);
}

React test conditional rendering using the useState hook based on API response

I have this React component that has a block that renders based on an API response.
const SomeComponent = () => {
const [render, setRender] = useState(false)
const handleClick = async () => {
const someFunction = await callToAPI()
if (someFunction) {
setRender(true)
}
}
return (
<main>
<button onClick={handleClick}></button>
<ShouldRender when={render} data-test-id='should-render'>
Something
</ShouldRender>
</main>
)
}
As I'm writing the test for it, I'm confused on how to test this conditional rendering.
I'd like to know if there's a way to use something like the fireEvent to set this state in my SomeComponent.test.js file.
So far the test looks like this:
test('renders component when button is clicked', () => {
render(<SomeComponent />)
const button = screen.queryByRole('button')
fireEvent.click(button)
const conditionalComponent = screen.getByTestId('should-render')
expect(conditionalComponent).toBeInTheDocument()
})

conditional rendering with toast and usestate does not work with react

I have my state and I want to display the component if the value is true but in the console I receive the error message Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state my code
import React, { useState} from "react";
import { useToasts } from "react-toast-notifications";
const Index = () => {
const [test, setTest]= useState(true);
const { addToast } = useToasts();
function RenderToast() {
return (
<div>
{ addToast('message') }
</div>
)}
return (
<div>
{test && <RenderToast /> }
</div>
)
}
You cannot set state during a render. And I'm guessing that addToast internally sets some state.
And looking at the docs for that library, you don't explicitly render the toasts. You just call addToast and then the <ToastProvider/> farther up in the tree shows them.
So to make this simple example works where a toast is shown on mount, you should use an effect to add the toast after the first render, and make sure your component is wrapped by <ToastProvider>
const Index = () => {
const { addToast } = useToasts();
useEffect(() => {
addToast('message')
}, [])
return <>Some Content here</>
}
// Example app that includes the toast provider
const MyApp = () => {
<ToastProvider>
<Index />
</ToastProvider>
}
how i can display the toast based on a variable for exemple display toast after receive error on backend?
You simply call addToast where you are handling your server communication.
For example:
const Index = () => {
const { addToast } = useToasts();
useEffect(() => {
fetchDataFromApi()
.then(data => ...)
.catch(error => addToast(`error: ${error}`))
}, [])
//...
}

Custom React hook, infinite loop only if I add the second dependency. Bug or something I can't understand?

I've made a really simple React hook. That's something seen on many guides and websites:
import { useEffect, useState } from 'react';
import axios from 'axios';
export const useFetchRemote = (remote, options, initialDataState) => {
const [data, setData] = useState(initialDataState);
useEffect(() => {
const fetchData = async () => {
const result = await axios.get(remote, options);
setData(result.data);
};
fetchData();
}, [remote]);
return data;
};
Example usage:
import { useFetchRemote } from '../utils';
export const UserList = () => {
const users = useFetchRemote('/api/users', {}, []);
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>}
</ul>
);
}
This is working. If I understand correctly:
With no dependencies like useEffect(() => { /*...*/ }), setting the state into the function would trigger a re-render, calling useEffect again, in an infinite loop.
With empty dependencies like useEffect(() => { /*...*/ }, []), my function will be called only the "very first time" component is mounted.
So, in my case, remote is a dependency. My function should be called again if remote changes. This is true also for options. If I add also options, the infinite loop starts. I can't understand... why this is happening?
export const useFetchRemote = (remote, options, initialDataState) => {
// ...
useEffect(() => {
// ...
}, [remote, options]);
// ...
};
The infinite loop is caused by the fact that your options parameter is an object literal, which creates a new reference on every render of UserList. Either create a constant reference by defining a constant outside the scope of UserList like this:
const options = {};
const initialDataState = [];
export const UserList = () => {
// or for variable options instead...
// const [options, setOptions] = useState({});
const users = useFetchRemote('/api/users', options, initialDataState);
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>}
</ul>
);
}
or if you intend the options parameter to be effectively constant for each usage of the userFetchRemote() hook, you can do the equivalent of initializing props into state and prevent the reference from updating on every render:
export const useFetchRemote = (remote, options, initialDataState) => {
const [optionsState] = useState(options);
const [data, setData] = useState(initialDataState);
useEffect(() => {
const fetchData = async () => {
const result = await axios.get(remote, optionsState);
setData(result.data);
};
fetchData();
}, [remote, optionsState]);
// ---------^
return data;
};
This second approach will prevent a new fetch from occuring though, if the options are dynamically changed on a particular call site of useFetchRemote().

Why does my useEffect react function run when the page loads although I am giving it a second value array

Why is my useEffect react function running on every page load although giving it a second value array with a query variable?
useEffect( () => {
getRecipes();
}, [query]);
Shouldn't it only run when the query state variable changes? I have nothing else using the getRecipes function except of the useEffect function.
import React, {useEffect, useState} from 'react';
import './App.css';
import Recipes from './components/Recipes/Recipes';
const App = () => {
// Constants
const APP_ID = '111';
const APP_KEY = '111';
const [recipes, setRecipes] = useState([]);
const [search, setSearch] = useState('');
const [query, setQuery] = useState('');
const [showRecipesList, setShowRecipesList] = useState(false);
// Lets
let recipesList = null;
// Functions
useEffect( () => {
getRecipes();
}, [query]);
// Get the recipie list by variables
const getRecipes = async () => {
const response = await fetch(`https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}&from=0&to=3&calories=591-722&health=alcohol-free`);
const data = await response.json();
console.log(data.hits);
setRecipes(data.hits);
}
// Update the search constant
const updateSearch = e => {
console.log(e.target.value);
setSearch(e.target.value);
}
const runQuery = e => {
e.preventDefault();
setQuery(search);
}
// List recipes if ready
if (recipes.length) {
console.log(recipes.length);
recipesList = <Recipes recipesList={recipes} />
}
return (
<div className="App">
<form className='search-app' onSubmit={ runQuery }>
<input
type='text'
className='search-bar'
onChange={ updateSearch }
value={search}/>
<button
type='submit'
className='search-btn' > Search </button>
</form>
<div className='recipesList'>
{recipesList}
</div>
</div>
);
}
export default App;
Following this: https://www.youtube.com/watch?v=U9T6YkEDkMo
A useEffect is the equivalent of componentDidMount, so it will run once when the component mounts, and then only re-run when one of the dependencies defined in the dependency array changes.
If you want to call getRecipes() only when the query dependency has a value, you can call it in a conditional like so:
useEffect(() => {
if(query) {
getRecipes()
}
}, [query])
Also, as your useEffect is calling a function (getRecipes) that is declared outside the use effect but inside the component, you should either move the function declaration to be inside the useEffect and add the appropriate dependencies, or wrap your function in a useCallback and add the function as a dependency of the useEffect.
See the React docs for information on why this is important.
UseEffect hook work equivalent of componentDidMount, componentDidUpdate, and componentWillUnmount combined React class component lifecycles.but there is a different in time of acting in DOM.componentDidMount and useEffect run after the mount. However useEffect runs after the paint has been committed to the screen as opposed to before. This means you would get a flicker if you needed to read from the DOM, then synchronously set state to make new UI.useLayoutEffect was designed to have the same timing as componentDidMount. So useLayoutEffect(fn, []) is a much closer match to componentDidMount() than useEffect(fn, []) -- at least from a timing standpoint.
Does that mean we should be using useLayoutEffect instead?
Probably not.
If you do want to avoid that flicker by synchronously setting state, then use useLayoutEffect. But since those are rare cases, you'll want to use useEffect most of the time.

Categories