How to update the state inside a function wrapped in useCallback hook - javascript

Having the following component:
import { useMemo, useState, useEffect, useCallback } from 'react';
import { CellProps } from 'react-table';
import {
useDataTable,
DataTable,
useOnRowClick,
useRowHighlight,
IconMenu
} from '../ui-components';
import { useGetTriggers } from '../../hooks';
import { ICalculationEngine } from '../../types';
export function MyComponent() {
const [selectedIndex, setSelectedIndex] = useState<null | number>(null);
const { data } = useGetTriggers();
const [isEditMode, setEditMode] = useState(false);
const [tableData, setTableData] = useState(data);
useEffect(() => {
setSelectedIndex(selectedIndex);
}, [selectedIndex, setSelectedIndex]);
const onRowClick = (triggerIndex: number) => {
setEditMode(false);
setSelectedIndex(triggerIndex);
};
const testFun = useCallback(() => {
setEditMode(!isEditMode);
}, [isEditMode]);
console.log('isEditMode! ', isEditMode);
const tableState = useDataTable({
columns: useMemo(
() => [
{
Header: 'Name',
accessor: 'name'
},
{
id: '1',
Cell: ({ cell }: CellProps<ICalculationEngine['Trigger']>) => {
if (cell.row.index === selectedIndex) {
return (
<IconMenu
items={[
{
title: 'edit',
onClick: testFun
}
]}
/>
);
}
return <div></div>;
}
}
],
[selectedIndex, testFun]
),
data: tableData,
options: {
onRowClick: onRowClick
},
plugins: [useOnRowClick, useRowHighlight]
});
useEffect(() => setTableData(data), [data]);
return <DataTable dataTableInstance={tableState} />;
}
export default MyComponent;
It is building a table with some data in DataTable which receives as props the content of the table, the columns: tableState. It only has 2 columns, one called Name and a clickable one.
When that one is clicked it must change that state of isEditMode from false to true.
There is a function called testFun which must to that but it doesn't work, the state isn't updating.
If the state is set to true by other actions, when the user clicks another row however, the state updates to false, in onRowClick function.
What is wrong with testFun that it doesn't update it? It is because of useCallback wrapping it? If I remove the wrapper, VS Code will say that:
The 'testFun' function makes the dependencies of useMemo Hook (at line
66) change on every render. Move it inside the useMemo callback.
Alternatively, wrap the definition of 'testFun' in its own
useCallback() Hook
and still it doesn't work, it doesn't update the state.

Related

How can I debounce editor value when the value is managed by parent component?

Editor component
import Editor from '#monaco-editor/react';
import { useDebounce } from './useDebounce';
import { useEffect, useState } from 'react';
type Props = {
code: string;
onChange: (code: string) => void;
disabled?: boolean;
};
export const GraphqlCodeEditor = ({
onChange,
code,
disabled = false,
}: Props) => {
const [editorValue, setEditorValue] = useState(code);
const editorValueDebounced = useDebounce(editorValue, 500);
useEffect(() => {
onChange(editorValueDebounced);
}, [editorValueDebounced, onChange]);
useEffect(() => {
if (code !== editorValueDebounced) {
setEditorValue(code);
}
}, [code, editorValueDebounced]);
return (
<Editor
options={{
minimap: { enabled: false },
autoClosingBrackets: 'always',
readOnly: disabled,
}}
language="graphql"
value={editorValue}
onChange={(value) => {
if (value) {
setEditorValue(value);
}
}}
/>
);
};
useDebounce hook
import { useEffect, useState } from 'react';
export const useDebounce = <T>(value: T, delay: number) => {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
};
The code prop in the editor component is managed by a parent component. When e.g a user loads a new code snippet it's updated and the editor should load the new value. The editor should also debounce it's value so the onChange() function isn't called on every keypress.
The editor component above results in a loop where the component switches between the previous value and the new every 500ms.
How can I achieve this with the useDebounce hook?
This is closely related to this post I think, however it's using lodash debounce. I was hoping to achieve the same with the useDebounce hook.
The problem was this useEffect()
useEffect(() => {
if (code !== editorValueDebounced) {
setEditorValue(code);
}
}, [code, editorValueDebounced]);
Changing it to this fixed the problem
useEffect(() => {
setEditorValue(code);
}, [code]);

State update not reflecting in custom hook

I am using react-redux to store a state modeData which is an array of objects. Using react-hotkeys-hook, when the user presses ctrl+l, a function runs which updates the state. In that same function, when I console.log the same state, it does not reflect the changes.
Here's the code I'm using:
import { useSelector, useDispatch } from "react-redux";
import { modeDataIncrement } from "./redux/modeSlice";
import { useHotkeys } from "react-hotkeys-hook";
import { useEffect } from "react";
//..
const modeData = useSelector((state) => state.mode.modeData); //array of objects.
const handler = (id) => {
const isActive = modeData.find((x) => x.id === id).active;
console.log(modeData); // does not show the updated state
dispatch(modeDataIncrement(id));
};
useHotkeys("ctrl+l", () => handler("Clone"));
useEffect(() => {
console.log(modeData); //working fine!
}, [modeData]);
modeSlice.js:
import { createSlice } from "#reduxjs/toolkit";
export const modeSlice = createSlice({
name: "mode",
initialState: {
modeData: [{ id: "Clone", active: false }],
},
reducers: {
modeDataIncrement: (state, action) => {
state.modeData.find(
(e) => e.id === action.payload && (e.active = !e.active)
);
},
},
});
export const { modeDataIncrement } = modeSlice.actions;
export default modeSlice.reducer;
Any thoughts on what I'm doing wrong?
Thanks!
Your code needs to use the latest version of modeData, but based on the source code of useHotkeys, it will memoize the function, and not update it automatically. So you're always using the version of the callback that existed on the first render.
To fix this, you need to pass a dependency array in to useHotkeys, so it can break the memoization:
const handler = (id) => {
const isActive = modeData.find((x) => x.id === id).active;
console.log(modeData);
dispatch(modeDataIncrement(id));
};
useHotkeys("ctrl+l", () => handler("Clone"), [modeData]);

How to use Hooks inside useEffect?

I wrote a demo here:
import React, { useRef, useEffect, useState } from "react";
import "./style.css";
export default function App() {
// let arrRef = [useRef(), useRef()];
let _data = [
{
title: A,
ref: null
},
{
title: B,
ref: null
}
];
const [data, setData] = useState(null);
useEffect(() => {
getDataFromServer();
}, []);
const getDataFromServer = () => {
//assume we get data from server
let dataFromServer = _data;
dataFromServer.forEach((e, i) => {
e.ref = useRef(null)
});
};
return (
<div>
{
//will trigger some function in child component by ref
data.map((e)=>(<div title={e.title} ref={e.ref}/>))
}
</div>
);
}
I need to preprocess after I got some data from server, to give them a ref property. the error says 'Hooks can only be called inside of the body of a function component' . so I checked the document, it says I can't use hooks inside a handle or useEffect. so is there a way to achieve what I need?
update:
I need to create component base on DB data, so when I create a component I need to give them a ref , I need trigger some function written in child component from their parent component and I use ref to achieve that. that is why I need to pass a ref to child component.

React push Api response in the setState

in my EventForm i have this const, this is a dialog form
this is my EventForm.js
const EventForm = (props) => {
const { setOpenPopup, records, setRecords, setMessage, setOpenSnackbar } = props
const addEvent = () => {
axios.post('https://jsonplaceholder.typicode.com/events', (event)
.then(resp => {
console.log(resp.data)
const newData = [{
title: resp.data.name,
start: resp.data.starts_at,
end: resp.data.ends_at
}]
setRecords([{ ...records, newData}])
//
setOpenPopup(false)
setMessage('New Event added')
setOpenSnackbar(true)
})
.catch([])
}
export default EventForm
EventForm.propTypes = {
setOpenPopup: PropTypes.func,
records: PropTypes.array,
setRecords: PropTypes.func,
setMessage: PropTypes.func,
setOpenSnackbar: PropTypes.func
}
}
in my EventTable.js
const [records, setRecords] = useState([]);
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/events')
.then(resp => {
const newData = resp.data.map((item) => ({
title: item.name,
start: item.starts_at,
end: item.ends_at
}))
setRecords(newData)
})
.catch(resp => console.log(resp))
}, [])
fullcalendar...
events={records}
im trying to push the API post response to my setRecords. so when the dialog form close it will not use the GET response. ill just get the new record and render to my view
but im getting an error:
Unhanded Rejection (TypeError): setRecords is not a function
I suspect you are using React Hooks. Make sure that your records state looks like this
const [records, setRecords] = useState([]);
In your axios request, it looks like that you are trying to spread the values of records which is an array to an object. I'd suggest refactoring this to something like this. Instead of trying to spread an array into the object, take the previous state and merge it with the new one.
setRecords(prevRecords => [...prevRecords, ...newData])
Here's an example using React Hooks how the component could look like
import React from "react";
import axios from "axios";
const MyComponent = ({
setOpenPopup,
records,
setRecords,
setMessage,
setOpenSnackbar
}) => {
const addEvent = () => {
axios
.post("https://jsonplaceholder.typicode.com/events", event) // Make sure this is defined somewhere
.then((resp) => {
const { name, starts_at, ends_at } = resp.data;
const newData = [
{
title: name,
start: starts_at,
end: ends_at
}
];
setRecords((prevRecords) => [...prevRecords, ...newData]);
setOpenPopup(false);
setMessage("New Event added");
setOpenSnackbar(true);
})
.catch([]);
};
return (
<div>
<button onClick={addEvent}>Click me </button>
</div>
);
};
export default MyComponent;
If you are not using React Hooks and use Class components, then make sure that you pass setRecords to your component in props. Plus, in your props destructuring, make sure you add this to the props, otherwise, it can lead to unwanted behaviour. Also, move your request function out of the render method and destructure values from the props that you need inside the function. I've also noticed that your axios syntax was incorrect (forgot to close after the event) so I fixed that as well. Here's an example of how you can improve it.
import React from "react";
import axios from "axios";
class MyComponent extends React.Component {
addEvent = () => {
const {
setOpenPopup,
setRecords,
setMessage,
setOpenSnackbar
} = this.props;
axios
.post("https://jsonplaceholder.typicode.com/events", event)
.then((resp) => {
console.log(resp.data);
const newData = [
{
title: resp.data.name,
start: resp.data.starts_at,
end: resp.data.ends_at
}
];
setRecords((prevRecords) => [...prevRecords, ...newData]);
//
setOpenPopup(false);
setMessage("New Event added");
setOpenSnackbar(true);
})
.catch([]);
};
render() {
return (
<div>
<button onClick={() => this.addEvent()}>Click me</button>
</div>
);
}
}
export default MyComponent;

React Hooks/Context & Elastictic UI. Problem with fetched data (REST) in function Component

I'm quite new to React Hooks/Context so I'd appreciate some help. Please don' t jump on me with your sharp teeth. I Checked other solutions and some ways i've done this before but can't seem to get it here with the 'pick from the list' way.
SUMMARY
I need to get the municipios list of names inside of my const 'allMunicipios'(array of objects) inside of my Search.js and then display a card with some data from the chosen municipio.
TASK
Get the data from eltiempo-net REST API.
Use Combobox async element from Elastic UI to choose from list of municipios.
Display Card (from elastic UI too) with some info of chosen municipio.
It has to be done with function components / hooks. No classes.
I'd please appreciate any help.
WHAT I'VE DONE
I've created my reducer, context and types files in a context folder to fecth all data with those and then access data from the component.
I've created my Search.js file. Then imported Search.js in App.js.
I've accesed the REST API and now have it in my Search.js
PROBLEM
Somehow I'm not beeing able to iterate through the data i got.
Basically i need to push the municipios.NOMBRE from api to the array const allMunicipios in my search.js component. But when i console log it it gives me undefined. Can;t figure out why.
I'll share down here the relevant code/components. Thanks a lot for whoever takes the time.
municipiosReducer.js
import {
SEARCH_MUNICIPIOS,
CLEAR_MUNICIPIOS,
GET_MUNICIPIO,
GET_WEATHER,
} from "./types";
export default (state, action) => {
switch (action.type) {
case SEARCH_MUNICIPIOS:
return {
...state,
municipios: action.payload,
};
case GET_MUNICIPIO:
return {
...state,
municipio: action.payload,
};
case CLEAR_MUNICIPIOS:
return {
...state,
municipios: [],
};
case GET_WEATHER: {
return {
...state,
weather: action.payload,
};
}
default:
return state;
}
};
municipiosContext.js
import { createContext } from "react";
const municipiosContext = createContext();
export default municipiosContext;
MunicipiosState.js
import React, { createContext, useReducer, Component } from "react";
import axios from "axios";
import MunicipiosContext from "./municipiosContext";
import MunicipiosReducer from "./municipiosReducer";
import {
SEARCH_MUNICIPIOS,
CLEAR_MUNICIPIOS,
GET_MUNICIPIO,
GET_WEATHER,
} from "./types";
const MunicipiosState = (props) => {
const initialState = {
municipios: [],
municipio: {},
};
const [state, dispatch] = useReducer(MunicipiosReducer, initialState);
//Search municipios
//In arrow functions 'async' goes before the parameter.
const searchMunicipios = async () => {
const res = await axios.get(
`https://www.el-tiempo.net/api/json/v2/provincias/08/municipios`
// 08 means barcelona province. This should give me the list of all its municipios
);
dispatch({
type: SEARCH_MUNICIPIOS,
payload: res.data.municipios,
});
};
//Get Municipio
const getMunicipio = async (municipio) => {
const res = await axios.get(
`https://www.el-tiempo.net/api/json/v2/provincias/08/municipios/${municipio.CODIGOINE}`
//CODIGOINE is in this REST API kind of the ID for each municipio.
//I intent to use this later to get the weather conditions from each municipio.
);
dispatch({ type: GET_MUNICIPIO, payload: res.municipio });
};
const dataMunicipiosArray = [searchMunicipios];
//Clear Municipios
const clearMunicipios = () => {
dispatch({ type: CLEAR_MUNICIPIOS });
};
return (
<MunicipiosContext.Provider
value={{
municipios: state.municipios,
municipio: state.municipio,
searchMunicipios,
getMunicipio,
clearMunicipios,
dataMunicipiosArray,
}}
>
{props.children}
</MunicipiosContext.Provider>
);
};
export default MunicipiosState;
Search.js
import "#elastic/eui/dist/eui_theme_light.css";
import "#babel/polyfill";
import MunicipiosContext from "../contexts/municipiosContext";
import MunicipiosState from "../contexts/MunicipiosState";
import { EuiComboBox, EuiText } from "#elastic/eui";
import React, { useState, useEffect, useCallback, useContext } from "react";
const Search = () => {
const municipiosContext = useContext(MunicipiosContext);
const { searchMunicipios, municipios } = MunicipiosState;
useEffect(() => {
return municipiosContext.searchMunicipios();
}, []);
const municipiosFromContext = municipiosContext.municipios;
const bringOneMunicipio = municipiosContext.municipios[0];
let municipiosNames = municipiosFromContext.map((municipio) => {
return { label: `${municipio.NOMBRE}` };
});
console.log(`municipiosFromContext`, municipiosFromContext);
console.log(`const bringOneMunicipio:`, bringOneMunicipio);
console.log(`municipiosNames:`, municipiosNames);
const allMunicipios = [
{ label: "santcugat" },
{ label: "BARCELONETA" },
{ label: "BARCE" },
];
const [selectedOptions, setSelected] = useState([]);
const [isLoading, setLoading] = useState(false);
const [options, setOptions] = useState([]);
let searchTimeout;
const onChange = (selectedOptions) => {
setSelected(selectedOptions);
};
// combo-box
const onSearchChange = useCallback((searchValue) => {
setLoading(true);
setOptions([]);
clearTimeout(searchTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
searchTimeout = setTimeout(() => {
// Simulate a remotely-executed search.
setLoading(false);
setOptions(
municipiosNames.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
}, 1200);
}, []);
useEffect(() => {
// Simulate initial load.
onSearchChange("");
}, [onSearchChange]);
return (
<div>
<EuiComboBox
placeholder="Search asynchronously"
async
options={options}
selectedOptions={selectedOptions}
isLoading={isLoading}
onChange={onChange}
onSearchChange={onSearchChange}
/>
<button>Lista de municipios</button>
</div>
);
};
export default Search;
also the
Home.js
import React, { useState } from "react";
import { EuiComboBox, EuiText } from "#elastic/eui";
// import { DisplayToggles } from "../form_controls/display_toggles";
import "#babel/polyfill";
import "#elastic/eui/dist/eui_theme_light.css";
import Search from "./Search";
import MunicipioCard from "./MunicipioCard";
const Home = () => {
return (
<div>
<EuiText grow={false}>
<h1>Clima en la provincia de Barcelona</h1>
<h2>Por favor seleccione un municipio</h2>
</EuiText>
<Search />
<MunicipioCard />
</div>
);
};
export default Home;
App.js
import "#babel/polyfill";
import "#elastic/eui/dist/eui_theme_light.css";
import { EuiText } from "#elastic/eui";
import React from "react";
import Home from "./components/Home";
import MunicipiosState from "./contexts/MunicipiosState";
import "./App.css";
function App() {
return (
<MunicipiosState>
<div className="App">
<EuiText>
<h1>App Component h1</h1>
</EuiText>
<Home />
</div>
</MunicipiosState>
);
}
export default App;
You are using forEach and assigning the returned value to a variable, however forEach doesn't return anything. You should instead use map
let municipiosNames = municipiosFromContext.map((municipio) => {
return `label: ${municipio.NOMBRE}`;
});
As per your comment:
you data is loaded asynchronously, so it won't be available on first render and since functional components depend on closures, you onSearchChange function takes the value from the closure at the time of creation and even if you have a setTimeout within it the updated value won't reflect
The solution here is to add municipiosFromContext as a dependency to useEffect
const onSearchChange = useCallback((searchValue) => {
setLoading(true);
setOptions([]);
clearTimeout(searchTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
searchTimeout = setTimeout(() => {
// Simulate a remotely-executed search.
setLoading(false);
setOptions(
municipiosNames.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
}, 1200);
}, [municipiosFromContext]);
useEffect(() => {
// Simulate initial load.
onSearchChange("");
}, [onSearchChange]);

Categories