Update a component after useState value updates - javascript

Having a monaco-editor inside a React component:
<Editor defaultValue={defaultValue} defaultLanguage='python' onChange={onChangeCode} />
The defaultValue, the default code inside of the editor, is sent via props to the component:
const MyComponent = ({
originalCode
}: MyComponentProps) => {
const [defaultValue, setDefaultValue] = useState(originalCode);
When the user edits the code, onChange={onChangeCode} is called:
const onChangeCode = (input: string | undefined) => {
if (input) {
setCode(input);
}
};
My question is, how to reset the code to the original one when the user clicks on Cancel?
Initially it was like:
const handleCancel = () => {
onChangeCode(defaultValue);
};
but it didn't work, probably because useState is asynchronous, any ideas how to fix this?
Here is the whole component for more context:
import Editor from '#monaco-editor/react';
import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Button, HeaderWithButtons } from '../shared/ui-components';
import { ICalculationEngine } from '../../../lib/constants/types';
import { usePostScript } from '../../../lib/hooks/use-post-script';
import { scriptPayload } from '../../../mocks/scriptPayload';
import { editorDefaultValue } from '../../../utils/utils';
export interface ScriptDefinitionProps {
realInputDetails: Array<ICalculationEngine['RealInputDetails']>;
realOutputDetails: ICalculationEngine['RealInputDetails'];
originalCode: string;
scriptLibId: string;
data: ICalculationEngine['ScriptPayload'];
}
const ScriptDefinition = ({
realInputDetails,
realOutputDetails,
originalCode
}: ScriptDefinitionProps) => {
const [defaultValue, setDefaultValue] = useState(originalCode);
const [code, setCode] = useState(defaultValue);
const { handleSubmit } = useForm({});
const { mutate: postScript } = usePostScript();
const handleSubmitClick = handleSubmit(() => {
postScript(scriptPayload);
});
const handleCancel = () => {
onChangeCode(defaultValue);
};
const onChangeCode = (input: string | undefined) => {
if (input) {
setCode(input);
}
};
useEffect(() => {
setDefaultValue(editorDefaultValue(realInputDetails, realOutputDetails));
}, [realInputDetails, realOutputDetails, originalCode]);
return (
<div>
<HeaderWithButtons>
<div>
<Button title='cancel' onClick={handleCancel} />
<Button title='save' onClick={handleSubmitClick} />
</div>
</HeaderWithButtons>
<Editor defaultValue={defaultValue} defaultLanguage='python' onChange={onChangeCode} />
</div>
);
};
export default ScriptDefinition;

If you need the ability to change the value externally, you'll need to use the Editor as a controlled component by passing the value prop (sandbox):
For example:
const defaultValue = "// let's write some broken code 😈";
function App() {
const [value, setValue] = useState(defaultValue);
const handleCancel = () => {
setValue(defaultValue);
};
return (
<>
<button title="cancel" onClick={handleCancel}>
Cancel
</button>
<Editor
value={value}
onChange={setValue}
height="90vh"
defaultLanguage="javascript"
/>
</>
);
}

Related

Why Need to declare twice in Context

I have just started learning TypeScript, and have been working on a small web app.
Due to state management, I have created a file called CountdownContext.tsx
import { useState, createContext, useContext, SetStateAction } from "react";
import { CountdownContextType } from "./types";
const CountdownContext = createContext({});
export const useCountdownContext = () => {
return useContext(CountdownContext) as CountdownContextType;
};
// React.useContext(TodoContext) as TodoContextType;
interface Props {
children: React.ReactNode;
}
export const CountdownContextProvider: React.FC<Props> = ({ children }) => {
const [eventDate, setEventDate] = useState<Date>(new Date());
function handleEventDate(e: React.ChangeEvent<HTMLInputElement>) {
setEventDate(new Date(e.target.value));
}
return (
<CountdownContext.Provider value={{ eventDate, handleEventDate }}>
{children}
</CountdownContext.Provider>
);
};
And I imported eventDate and handleEventDate in the file below.
import React, { useState } from "react";
import { useCountdownContext } from "../../contexts/CountdownContext";
type Props = {
activeEdit: boolean;
};
const EventDate: React.FC<Props> = ({ activeEdit }) => {
const { eventDate, handleEventDate } = useCountdownContext();
return (
<div>
{activeEdit ? (
<input type="date" onChange={(e) => handleEventDate(e.target.value, typeof new Date(e.target.value))} />
) : (
<div>{eventDate}</div>
)}
</div>
);
};
export default EventDate;
However, I got an error when importing. After searching about the problem happened,
I figured out that I needed to declare types.
Then I created a file below
export type CountdownContextType = {
eventDate: Date;
handleEventDate: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
However, one concern is that I thought I already provided and declared types for eventDate and handleEventDate in the context file.
Thus, I thought to question why I gotta do this twice??
Please give me a clarification about this problem. Thanks so much for your kindness and help.
One way to not using types is to create initial value for your context like this:
import * as React from "react";
import ReactDOM from "react-dom";
import { useState, createContext, useContext } from "react";
const initialValue = {
eventDate: new Date(),
handleEventDate: (e: React.ChangeEvent<HTMLInputElement>) => {},
};
const CountdownContext = createContext(initialValue);
const useCountDownState = () => {
const [eventDate, setEventDate] = useState<Date>(new Date());
function handleEventDate(e: React.ChangeEvent<HTMLInputElement>) {
console.log("setting date");
setEventDate(new Date(e.target.value));
}
return { eventDate, handleEventDate };
};
const CountdownContextProvider = ({ children }: { children: JSX.Element }) => {
return (
<CountdownContext.Provider value={useCountDownState()}>
{children}
</CountdownContext.Provider>
);
};
const useCountdownContext = () => useContext(CountdownContext);
const EventDate = ({ activeEdit }: { activeEdit: boolean }) => {
const { eventDate, handleEventDate } = useCountdownContext();
return (
<div>
{activeEdit ? (
<input type="date" onChange={handleEventDate} />
) : (
<div>{eventDate.toDateString()}</div>
)}
</div>
);
};
function App() {
return (
<CountdownContextProvider>
<EventDate activeEdit />
</CountdownContextProvider>
);
}
ReactDOM.render(<App />, document.querySelector("#root"));
The code is heavily inspired by:
https://youtu.be/P95DuIBwnqw?t=822
And here is the source code from the link:
https://github.com/jherr/which-react-state-manager/blob/main/examples/hooks-context/src/store.tsx

While rendering a component it is showing an error- "Cannot update a component (`App`) while rendering a different component (`EventList`). "

I Can't render my events. Its showing this error -
"Cannot update a component (App) while rendering a different component (EventList). To locate the bad setState() call inside EventList, follow the stack trace as described in https://reactjs.org/link/setstate-in-render"
Here is EventList Component code -
import { useEffect, useState } from "react";
import EventList from "../../event-list";
import EventForm from "../event-form";
const EventAction = ({
getEventsByClockID,
addEvent,
updateEvent,
clockID,
deleteEvent,
deleteEventsByClockID,
}) => {
const [isCreate, setIsCreate] = useState(false);
const [isToggle, setIsToggle] = useState(false);
const [eventState, setEventState] = useState(null)
const handleCreate = () => {
setIsCreate(!isCreate);
}
useEffect(() => {
setEventState(getEventsByClockID(clockID, true));
}, [isToggle])
const handleToggle = () => {
setIsToggle(!isToggle);
}
return (
<div>
<div>
<button onClick={handleCreate}>Create Event</button>
<button onClick={handleToggle}>Toggle Events</button>
</div>
{isCreate && (
<>
<h3>Create Event</h3>
<EventForm
clockID={clockID}
handleEvent={addEvent}
/>
</>
)}
{isToggle && (
<>
<h3>Events of this clock</h3>
<EventList
clockID={clockID}
eventState={eventState}
deleteEvent={deleteEvent}
updateEvent={updateEvent}
deleteEventsByClockID={deleteEventsByClockID}
/>
</>
)}
</div>
)
}
export default EventAction;
Here is my App Component Code -
import ClockList from "./components/clock-list";
import LocalClock from "./components/local-clock";
import useApp from "./hooks/useApp";
import { localClockInitState } from "./initialStates/clockInitState";
const App = () => {
const {
localClock,
clocks,
updateLocalClock,
createClock,
updateClock,
deleteClock,
getEventsByClockID,
addEvent,
deleteEvent,
updateEvent,
deleteEventsByClockID,
} = useApp(localClockInitState);
return (
<div>
<LocalClock
clock={localClock}
updateClock={updateLocalClock}
createClock={createClock}
/>
<ClockList
clocks={clocks}
localClock={localClock.date}
updateClock={updateClock}
deleteClock={deleteClock}
getEventsByClockID={getEventsByClockID}
addEvent={addEvent}
deleteEvent={deleteEvent}
updateEvent={updateEvent}
deleteEventsByClockID={deleteEventsByClockID}
/>
</div>
)
}
export default App;
and Here is my useApp hook -
import { useState } from "react";
import deepClone from "../utils/deepClone";
import generateID from "../utils/generateId";
import useEvents from "./useEvents";
const getID = generateID('clock');
const useApp = (initValue) => {
const [localClock, setLocalClock] = useState(deepClone(initValue));
const [clocks, setClocks] = useState([]);
const {
// events,
// getEvents,
getEventsByClockID,
addEvent,
deleteEvent,
deleteEventsByClockID,
updateEvent,
} = useEvents();
const updateLocalClock = (data) => {
setLocalClock({
...localClock,
...data,
})
}
const createClock = (clock) => {
clock.id = getID.next().value;
setClocks((prev) => ([
...prev, clock
]))
}
const updateClock = (updatedClock) => {
setClocks(clocks.map(clock => {
if(clock.id === updatedClock.id) return updatedClock;
return clock;
}));
}
const deleteClock = (id) => {
setClocks(clocks.filter(clock => clock.id !== id));
}
return {
localClock,
clocks,
updateLocalClock,
createClock,
updateClock,
deleteClock,
getEventsByClockID,
addEvent,
deleteEvent,
updateEvent,
deleteEventsByClockID,
}
}
export default useApp;
I want to show all events incorporated with each individual clock.

How can I send the state (useState) of one file component to another file's component?

REACT.js:
Let say I have a home page with a search bar, and the search bar is a separate component file i'm calling.
The search bar file contains the useState, set to whatever the user selects. How do I pull that state from the search bar and give it to the original home page that
SearchBar is called in?
The SearchBar Code might look something like this..
import React, { useEffect, useState } from 'react'
import {DropdownButton, Dropdown} from 'react-bootstrap';
import axios from 'axios';
const StateSearch = () =>{
const [states, setStates] = useState([])
const [ stateChoice, setStateChoice] = useState("")
useEffect (()=>{
getStates();
},[])
const getStates = async () => {
let response = await axios.get('/states')
setStates(response.data)
}
const populateDropdown = () => {
return states.map((s)=>{
return (
<Dropdown.Item as="button" value={s.name}>{s.name}</Dropdown.Item>
)
})
}
const handleSubmit = (value) => {
setStateChoice(value);
}
return (
<div>
<DropdownButton
onClick={(e) => handleSubmit(e.target.value)}
id="state-dropdown-menu"
title="States"
>
{populateDropdown()}
</DropdownButton>
</div>
)
}
export default StateSearch;
and the home page looks like this
import React, { useContext, useState } from 'react'
import RenderJson from '../components/RenderJson';
import StateSearch from '../components/StateSearch';
import { AuthContext } from '../providers/AuthProvider';
const Home = () => {
const [stateChoice, setStateChoice] = useState('')
const auth = useContext(AuthContext)
console.log(stateChoice)
return(
<div>
<h1>Welcome!</h1>
<h2> Hey there! Glad to see you. Please login to save a route to your prefered locations, or use the finder below to search for your State</h2>
<StateSearch stateChoice={stateChoice} />
</div>
)
};
export default Home;
As you can see, these are two separate files, how do i send the selection the user makes on the search bar as props to the original home page? (or send the state, either one)
You just need to pass one callback into your child.
Homepage
<StateSearch stateChoice={stateChoice} sendSearchResult={value => {
// Your Selected value
}} />
Search bar
const StateSearch = ({ sendSearchResult }) => {
..... // Remaining Code
const handleSubmit = (value) => {
setStateChoice(value);
sendSearchResult(value);
}
You can lift the state up with function you pass via props.
const Home = () => {
const getChoice = (choice) => {
console.log(choice);
}
return <StateSearch stateChoice={stateChoice} giveChoice={getChoice} />
}
const StateSearch = (props) => {
const handleSubmit = (value) => {
props.giveChoice(value);
}
// Remaining code ...
}
Actually there is no need to have stateChoice state in StateSearch component if you are just sending the value up.
Hello and welcome to StackOverflow. I'd recommend using the below structure for an autocomplete search bar. There should be a stateless autocomplete UI component. It should be wrapped into a container that handles the search logic. And finally, pass the value to its parent when the user selects one.
// import { useState, useEffect } from 'react' --> with babel import
const { useState, useEffect } = React // --> with inline script tag
// Autocomplete.jsx
const Autocomplete = ({ onSearch, searchValue, onSelect, suggestionList }) => {
return (
<div>
<input
placeholder="Search!"
value={searchValue}
onChange={({target: { value }}) => onSearch(value)}
/>
<select
value="DEFAULT"
disabled={!suggestionList.length}
onChange={({target: {value}}) => onSelect(value)}
>
<option value="DEFAULT" disabled>Select!</option>
{suggestionList.map(({ id, value }) => (
<option key={id} value={value}>{value}</option>
))}
</select>
</div>
)
}
// SearchBarContainer.jsx
const SearchBarContainer = ({ onSelect }) => {
const [searchValue, setSearchValue] = useState('')
const [suggestionList, setSuggestionList] = useState([])
useEffect(() => {
if (searchValue) {
// some async logic that fetches suggestions based on the search value
setSuggestionList([
{ id: 1, value: `${searchValue} foo` },
{ id: 2, value: `${searchValue} bar` },
])
}
}, [searchValue, setSuggestionList])
return (
<Autocomplete
onSearch={setSearchValue}
searchValue={searchValue}
onSelect={onSelect}
suggestionList={suggestionList}
/>
)
}
// Home.jsx
const Home = ({ children }) => {
const [result, setResult] = useState('')
return (
<div>
<SearchBarContainer onSelect={setResult} />
result: {result}
</div>
)
}
ReactDOM.render(<Home />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Just pass a setState to component
parent component:
const [state, setState] = useState({
selectedItem: ''
})
<StateSearch state={state} setState={setState} />
change parent state from child component:
const StateSearch = ({ state, setState }) => {
const handleStateChange = (args) => setState({…state, selectedItem:args})
return (...
<button onClick={() => handleStateChange("myItem")}/>
...)
}

How to use generic types correctly in HOC components?

Here is my parent component (Grid), (here I pass more props used by hoc but I ommited them here):
<QuickBooksTable itemList={quickBooksList} />
Here is the table component:
export const QuickBooksTable = withIntegrationTable(props: : WithIntegrationTableChildProps & WithIntegrationTableProps) => ...
Here is the hoc:
export function withIntegrationTable<T extends WithIntegrationTableProps>(Component: React.ComponentType<T>) {
return (
{
itemList,
...props
}: WithIntegrationTableProps & T
) => {
const [state, setState] = useState<WithIntegrationTableState>({
tableItems: new Array<any>(),
selectedItems: new Set<string>(),
isAllItemsSelected: false
});
useEffect(() => {
const tableItems = mapItemList(itemList, currentUser);
setState({
...state,
tableItems
});
}, [itemList]);
<Component {...props as T}
tableState={state}
/>
}
}
But when it compiles it says:Element QuickBooksTable doesn't have required attribute (here is another props name).
How should I use the types and generics to remove this error? I've tried to read the docs but I can't understand what I am doing wrong.
I think this is what you try to achieve.
import React from 'react';
import { useEffect, useState } from 'react';
interface WithIntegrationTableProps {
itemList: string[]
}
interface WithIntegrationTableState { }
export const withIntegrationTable = <P extends WithIntegrationTableState>(
Component: React.ComponentType<P & {
tableState: WithIntegrationTableState
}>
): React.FC<P & WithIntegrationTableProps> => ({
itemList,
...props
}: WithIntegrationTableProps) => {
const mapItemList = (itemList: any, currentUser: any) => {
}
const [state, setState] = useState<WithIntegrationTableState>({
tableItems: new Array<any>(),
selectedItems: new Set<string>(),
isAllItemsSelected: false
});
useEffect(() => {
const tableItems = mapItemList(itemList, null);
setState({
...state,
tableItems
});
}, [itemList]);
return <Component {...props as P} tableState={state} />
}
export const QuickBooksTable = withIntegrationTable(({ ...props }) => {
console.log(props.tableState)
return <div>
test
</div>
})
const App = () => {
return <QuickBooksTable itemList={[]} />
}
export default App

How to debounce a controlled input?

I'm currently struggling with react inputs and debounce from lodash.
Most of the time when I have a form I also have an edit option, so I need a controlled component to fill back the inputs using value={state["targetValue"]} so I can fill and edit the field.
However, if the component is controlled debounce isn't working.
I made a simple example on CodeSandbox: https://codesandbox.io/embed/icy-cloud-ydzj2?fontsize=14&hidenavigation=1&theme=dark
Code:
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { debounce } from "lodash";
import "./styles.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
name: "",
title: "",
editMode: false
};
this.debouncedEvent = React.createRef();
}
debounceEvent(_fn, timer = 500, options = null) {
this.debouncedEvent.current = debounce(_fn, timer, options);
return e => {
e.persist();
return this.debouncedEvent.current(e);
};
}
componentWillUnmount() {
this.debouncedEvent.current.cancel();
}
onChangeValue = event => {
const { name, value } = event.target;
this.setState(() => {
return { [name]: value };
});
};
onRequestEdit = () => {
this.setState({ name: "Abla blabla bla", editMode: true });
};
onCancelEdit = () => {
if (this.state.editMode) this.setState({ name: "", editMode: false });
};
onSubmit = event => {
event.preventDefault();
console.log("Submiting", this.state.name);
};
render() {
const { name, editMode } = this.state;
const isSubmitOrEditLabel = editMode ? `Edit` : "Submit";
console.log("rendering", name);
return (
<div className="App">
<h1> How to debounce controlled input ?</h1>
<button type="button" onClick={this.onRequestEdit}>
Fill with dummy data
</button>
<button type="button" onClick={this.onCancelEdit}>
Cancel Edit Mode
</button>
<div style={{ marginTop: "25px" }}>
<label>
Controlled / Can be used for editing but not with debounce
</label>
<form onSubmit={this.onSubmit}>
<input
required
type="text"
name="name"
value={name}
placeholder="type something"
// onChange={this.onChangeValue}
onChange={this.debounceEvent(this.onChangeValue)}
/>
<button type="submit">{isSubmitOrEditLabel}</button>
</form>
</div>
<div style={{ marginTop: "25px" }}>
<label> Uncontrolled / Can't be used for editing </label>
<form onSubmit={this.onSubmit}>
<input
required
type="text"
name="name"
placeholder="type something"
onChange={this.debounceEvent(this.onChangeValue)}
/>
<button type="submit">{isSubmitOrEditLabel}</button>
</form>
</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
At first it looks impossible, but there is a simple way to do it.
Just create the debounce function expression outside the react component.
Here is a pseudo example, for modern React with Hooks:
import React, { useState, useEffect } from "react";
import { debounce } from "lodash";
...
const getSearchResults = debounce((value, dispatch) => {
dispatch(getDataFromAPI(value));
}, 800);
const SearchData = () => {
const [inputValue, setInputValue] = useState("");
...
useEffect(() => {
getSearchResults(inputValue, dispatch);
}, [inputValue]);
...
return (
<>
<input
type="text"
placeholder="Search..."
value={inputValue}
onChange={e => setInputValue(e.target.value)}
/>
...
</>
);
};
export default SearchData;
Update: In time I came up with a better solution using "useCallback"
import React, { useState, useEffect, useCallback } from "react";
import { debounce } from "lodash";
...
const SearchData = () => {
const [inputValue, setInputValue] = useState("");
...
const getSearchResults = useCallback(
debounce(value => {
dispatch(getDataFromAPI(value));
}, 800),
[]
);
useEffect(() => {
getSearchResults(inputValue);
}, [inputValue]);
...
return (
<>
<input
type="text"
placeholder="Search..."
value={inputValue}
onChange={e => setInputValue(e.target.value)}
/>
...
</>
);
};
So... Apparently, there's no solution. the input takes the value from the state. While debounce prevents the state to trigger.
I made a workaround using ReactDOM.
import ReactDOM from "react-dom";
export const setFormDefaultValue = (obj, ref) => {
if (ref && !ref.current) return;
if (!obj || !obj instanceof Object) return;
const _this = [
...ReactDOM.findDOMNode(ref.current).getElementsByClassName("form-control")
];
if (_this.length > 0) {
_this.forEach(el => {
if (el.name in obj) el.value = obj[el.name];
else console.error(`Object value for ${el.name} is missing...`);
});
}
};
and then the use:
this.refForm = React.createRef();
setFormDefaultValue(this.state, refForm)
This way I can fill my form with the state default value and continue using debounce.
Take a look at this lib: https://www.npmjs.com/package/use-debounce
Here's an example of how to use it:
import React, { useState } from 'react';
import { useDebounce } from 'use-debounce';
export default function Input() {
const [text, setText] = useState('Hello');
const [value] = useDebounce(text, 1000);
return (
<div>
<input
defaultValue={'Hello'}
onChange={(e) => {
setText(e.target.value);
}}
/>
<p>Actual value: {text}</p>
<p>Debounce value: {value}</p>
</div>
);
}
You can try this.
import React, { useState, useCallback, useRef, useEffect } from 'react';
import _ from 'lodash';
function usePrevious(value: any) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const DeboucnedInput = React.memo(({ value, onChange }) => {
const [localValue, setLocalValue] = useState('');
const prevValue = usePrevious(value);
const ref = useRef();
ref.current = _.debounce(onChange, 500);
useEffect(() => {
if (!_.isNil(value) && prevValue !== value && localValue !== value) {
setLocalValue(value);
}
}, [value]);
const debounceChange = useCallback(
_.debounce(nextValue => {
onChange(nextValue);
}, 1000),
[]
);
const handleSearch = useCallback(
nextValue => {
if (nextValue !== localValue) {
setLocalValue(nextValue);
debounceChange(nextValue);
}
},
[localValue, debounceChange]
);
return (
<input
type="text"
value={localValue}
onChange={handleSearch}
/>
);
});
I had a similar issue, useMemo and debounce from lodash seem to work for me.
import React, { useState, useEffect, useMemo } from 'react';
import { debounce } from 'lodash';
const Search = () => {
const [inputValue, setInputValue] = useState('');
const handleChange = (event) => {
setInputValue(event.target.value);
};
const handleFetch = (input) => {
// do stg
};
const debouncedFetch = useMemo(() => debounce(handleFetch, 500), []);
useEffect(() => {
debouncedFetch(inputValue);
}, [inputValue]);
return (
<Form>
<input
type="text"
value={inputValue}
onChange={handleChange}
/>
// rest of the component
</Form>
);
};
Some articles that helped me:
https://dev.to/alexdrocks/using-lodash-debounce-with-react-hooks-for-an-async-data-fetching-input-2p4g
https://www.carlrippon.com/using-lodash-debounce-with-react-and-ts/
https://blog.logrocket.com/how-and-when-to-debounce-or-throttle-in-react/

Categories

Resources