React - Controlled input props are outdated in useCallback - react-hooks

I have a controlled input where its value and onChange event are set via props. When the selectedValue prop changes, onInputChanged callback gets triggered, but if I try accessing selectedValue in this callback it's out of date.
Seems like when selectedValue changes, onInputChanged gets called before it has been updated with the new dependencies
I'm probably just not understanding the lifecycle of the component correctly, but is there a way to ensure that I can access the latest selectedValue from inside the onChange event? Here's my code:
const TestInput = ({ onChange, selectedValue }) => {
const onInputChanged = React.useCallback(content => {
// Will not log the latest selectedValue
console.log(selectedValue);
onChange(content.target.value);
}, [selectedValue]);
return (
<input value={selectedValue} onChange={onInputChanged} placeholder="Test...." />
);
};
const TestContainer = () => {
const [value, setValue] = React.useState('');
return <TestInput selectedValue={value} onChange={setValue} />;
};
Heres a sandbox link to play around with - https://playcode.io/1042642

It is not possible to access it from the onChange function, since the function will not be rendered twice to show the new value, you could check it in a useEffect here documentation link.
You could try something like this:
const TestInput = ({ onChange, selectedValue }) => {
useEffect(() => {
console.log('selected value', selectedValue);
}, [selectedValue]);
const onInputChanged = React.useCallback(content => {
onChange(content.target.value);
}, [selectedValue]);
return (
<input value={selectedValue} onChange={onInputChanged} placeholder="Test...." />
);
};
const TestContainer = () => {
const [value, setValue] = React.useState('');
return <TestInput selectedValue={value} onChange={setValue} />;
};

Related

Is it possible to BehaviorSubject reset my useState value?

Problem: Whenever I click 't' key my theme changed as I expected. But for some reason this cause isModalVisible state reset. This cause one problem - When I have my modal open and click t - the modal disapear. I don't know why. Maybe BehaviorSubject cause the problem? Additionally, if I create another useState for test, which initial value is string 'AAA', and on open modal I set this state to string 'BBB'. Then I click 't' to change theme this test useState back to beginning value 'AAA'
My code looks like this:
const Component1 = () => {
const [mode, setMode] = useRecoilState(selectedModeState);
const { switcher, status } = useThemeSwitcher();
const toggleMode = (newTheme: MODE_TYPE) => {
theme$.next({ theme: newTheme });
localStorage.setItem('theme', newTheme);
};
useEffect(() => {
const mode_sub = theme$.subscribe(({ theme }) => {
switcher({ theme: theme });
setMode(theme);
});
return () => {
mode_sub.unsubscribe();
};
}, []);
return <Component2 toggleMode={toggleMode} currentMode={mode} />
}
const Component2 = ({toggleMode, currentMode}) => {
const [mode, setMode] = useState(currentMode);
const [savedTheme, setSavedTheme] = useRecoilState(selectedThemeState);
const getTheme = mode === MODE_TYPE.LIGHT ? MODE_TYPE.DARK : MODE_TYPE.LIGHT;
const themeListener = (event: KeyboardEvent) => {
switch (event.key) {
case 't':
setMode(getTheme);
toggleMode(getTheme);
break;
}
};
useEffect(() => {
document.addEventListener('keydown', themeListener);
document.body.setAttribute('data-theme', savedTheme);
localStorage.setItem('color', savedTheme);
return () => {
document.removeEventListener('keydown', themeListener);
};
}, [savedTheme]);
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
return (
<Modal isModalVisible={isModalVisible} setIsModalVisible={setIsModalVisible} />
)
}
UTILS:
export const selectedModeState = atom<MODE_TYPE>({
key: 'selectedThemeState',
default: (localStorage.getItem('theme') as MODE_TYPE) || MODE_TYPE.LIGHT,
});
export const selectedThemeState = atom<string>({
key: 'selectedColorState',
default: (localStorage.getItem('color') as string) || 'blue',
});
export const theme$ = new BehaviorSubject({
theme: (localStorage.getItem('theme') as THEME_TYPE) || THEME_TYPE.LIGHT,
});
I would like the theme change not to set visibleModal to false which causes the modal to close

Cannot be used as a JSX component. Its return type 'Promise<Element>' is not a valid JSX element

I have an React Component Editor. I am trying to initialize the state using an async function. But I am unable to .
How we can do that in React.
const Editor = () => {
const { id } = useParams();
const [schemas, updateSchemas] = useAtom(bfsAtom);
const schema = id && _.get(schemas, id, {});
type InitialStateType = {
properties: KeyedProperty[];
validations: ValidationDataProperty[];
};
const getInitialState = async (): Promise<InitialStateType> => {
return {
properties: createPropertiesFromSchema(schema),
validations: initializeConditions(schema),
};
};
const initialState = await getInitialState();
const mainReducer = (
{ properties, validations }: InitialStateType,
action: Action
) => ({
properties: propertyReducer(properties, action),
validations: validationReducer(validations, action),
});
const [state, dispatch] = useReducer(mainReducer, initialState);
return (
<PropertyContext.Provider value={{ state, dispatch }}>
<SchemaEditor schema={schema} />
</PropertyContext.Provider>
);
};
TL;DR moving the async from top level to inside the component did the trick
Is not directly linked with your question but one thing that I was doing wrong is the following:
I had a component that looked like:
const MyComponent = async () => {
...
const apiResponse = await apiFunction();
return <div>
{apiResponse.success && (
<p>It was a success!</p>
)}
</div>
}
BUT you shouldn't use async in your components at top level. So to fix it I did this:
const MyComponent = () => {
...
const apiResponse = async () => {
return await apiFunction();
}
return <div>
{apiResponse().success && (
<p>It was a success!</p>
)}
</div>
}

Uncaught TypeError: Cannot read properties of undefined (reading 'target'),

My problem is that how do i access 'handleInputChange', because i cant write 'handleInputChange' function outside the useEffect hook since it is performing a sideeffect. I would love it if someone can help me out with this situation.
1. const [values, setValue] = useState({});
const dispatch = useDispatch();
let handleInputChange
useEffect(()=>{
handleInputChange = (e) =>{
setValue(
{
values: { ...values, [e.target.name]: e.target.value }
},
() => {
dispatch({
type: "SET_FORMVALUES",
payload: values
})
}
)}
handleInputChange();
},[dispatch])
<TextField id="outlined-basic" label="First Name" variant="outlined"
name="firstName"
className='app__input'
placeholder='First Name'
type="text"
value={values['firstName']}
onChange = {handleInputChange} />
//Reducer.js
const initialState = {
formValues: {},
message: ""
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case "SET_FORMVALUES":
console.log("--- Changing form data ---");
return {
...state,
formValues: action.payload
};
case "SUBMIT_FORM":
return {
...state,
message: "Form submitted!!"
};
default:
return state;
}
};
First, you don't need the core React useState hook, because you are using React Redux. This is actually creating a second state local to your component. Instead, use the centralized Redux store you've configured and React-Redux hooks. As long as you have wrapped your app in a context provider and passed your store to it, you can use useDispatch to update state and useSelector to retrieve state and subscribe to changes.
Second, you don't need useEffect, as you are just updating state.
Here's an example:
import { useDispatch, useSelector } from 'react-redux';
export default function App() {
const formValues = useSelector((state) => state.formValues);
const dispatch = useDispatch();
const handleInputChange = (name, value) => {
dispatch(
{
type: "SET_FORMVALUES",
payload: {
...formValues,
[name]: value
}
}
);
}
return (
<div className="App">
<input type="text" name="FirstName" onChange={ (e) => handleInputChange(e.target.name, e.target.value)} />
<span>{formValues["FirstName"]}</ span>
<input type="text" name="LastName" onChange={ (e) => handleInputChange(e.target.name, e.target.value)} />
<span>{formValues["LastName"]}</ span>
</div>
);
}
Much of this is probably not directly related to the error in the question title, but simplifying your code should help you debug more easily. That error may have been simply because you didn't explicitly pass the event in your onChange handler. I.e. onChange = {handleInputChange} vs. onChange = {(e) => handleInputChange(e)}

Redux connected React component not updating until a GET api request is recalled

My react app uses a redux connected component to render data from backend for a project page, so I called a GET dispatch inside a React Hook useEffect to make sure data is always rendered when the project page first open, and whenever there is a change in state project, the component will be updated accordingly using connect redux function. However, the component doesn't update after I reduce the new state using a DELETE API request, only if I dispatch another GET request then the state will be updated. So I have to call 2 dispatches, one for DELETE and one for GET to get the page updated synchronously (as you can see in handleDeleteUpdate function), and the same thing happened when I dispatch a POST request to add an update (in handleProjectUpdate). Only when I reload the page, the newly changed data will show up otherwise it doesn't happen synchronously, anyone knows what's wrong with the state update in my code? and how can I fix this so the page can be loaded faster with only one request?
I've changed the reducer to make sure the state is not mutated and is updated correctly.
I have also tried using async function in handleDeleteUpdate to make sure the action dispatch is finished
I have tried
console.log(props.project.data.updates)
to print out the updates list after calling props.deleteUpdate but it seems the updates list in the state have never been changed, but when I reload the page, the new updates list is shown up
Here is the code I have for the main connected redux component, actions, and reducers file for the component
function Project(props) {
let options = {year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit'}
const {projectID} = useParams();
const history = useHistory();
console.log(props.project.data? props.project.data.updates : null);
console.log(props.project.data);
// const [updates, setUpdates] = useState(props.project.data? props.project.data.updates : null)
useEffect(() => {
props.getProject(projectID);
}, []);
// Add an update to project is handled here
const handleProjectUpdate = async (updateInfo) => {
await props.postProjectUpdate(projectID, updateInfo)
await props.getProject(projectID);
}
const handleDeleteUpdate = async (updateID) => {
await props.deleteUpdate(projectID, updateID);
await props.getProject(projectID);
console.log(props.project.data.updates);
};
return (
<div>
<Navbar selected='projects'/>
<div className = "project-info-layout">
<UpdateCard
updates = {props.project.data.updates}
handleProjectUpdate = {handleProjectUpdate}
handleDeleteUpdate = {handleDeleteUpdate}
options = {options}
/>
</div>
</div>
)
}
const mapStateToProps = state => ({
project: state.project.project,
});
export default connect(
mapStateToProps,
{getProject, postProjectUpdate, deleteUpdate}
)(Project);
ACTION
import axios from 'axios';
import { GET_PROJECT_SUCCESS,ADD_PROJECT_UPDATE_SUCCESS, DELETE_PROJECT_UPDATE_SUCCESS} from './types';
let token = localStorage.getItem("token");
const config = {
headers: {
Authorization: `Token ${token}`,
}
};
export const getProject = (slug) => dispatch => {
axios.get(`${backend}/api/projects/` + slug, config)
.then(
res => {
dispatch({
type: GET_PROJECT_SUCCESS,
payload: res.data,
});
},
).catch(err => console.log(err));
}
export const postProjectUpdate = (slug, updateData) => dispatch => {
axios.post(`${backend}/api/projects/`+slug+ `/updates`,updateData, config)
.then(
res => {
dispatch({
type: ADD_PROJECT_UPDATE_SUCCESS,
payload: res.data,
});
},
).catch(err => console.log(err));
}
export const deleteUpdate = (slug, updateID) => dispatch => {
axios.delete(`${backend}/api/projects/`+ slug + `/updates/`+ updateID, config)
.then(
res => {
dispatch({
type: DELETE_PROJECT_UPDATE_SUCCESS,
payload: updateID,
});
},
).catch(err => console.log(err));
}
Reducer
import { GET_PROJECT_SUCCESS,ADD_PROJECT_UPDATE_SUCCESS, DELETE_PROJECT_UPDATE_SUCCESS} from "../actions/types";
const initialState = {
project: {},
};
export default function ProjectReducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_PROJECT_SUCCESS:
return {
...state, // return all initial state
project: payload
};
case ADD_PROJECT_UPDATE_SUCCESS:
return {
...state,
project: {
...state.project,
updates: [...state.project.data.updates, payload.data]
}
};
case DELETE_PROJECT_UPDATE_SUCCESS:
let newUpdatesArray = [...state.project.updates]
newUpdatesArray.filter(update => update.uuid !== payload)
return {
...state,
project: {
...state.project,
members: newUpdatesArray
}
};
default:
return state;
}
}
updateCard in the Project component is showing a list of all updates

override one value with a new value gives value undefined

To summarize what I want to do:
Update the state depending on the previous state
I have searched in vain for a solution to the above problems. Found 3 solutions, unfortunately without any success.
1)
const Form = (props) => {
const [newValue, setNewValue] = useState(0);
const submitHandler = (e) => {
e.preventDefault();
const incrementOne = {
value: setNewValue((prevState) => {
return {...prevState, newValue: newValue + 1}
})
};
console.log(incrementOne);
};
const submitHandler = (e) => {
e.preventDefault();
const incrementOne = {
value: setNewValue(newValue + 1),
};
console.log(incrementOne);
};
3
const submitHandler = (e) => {
e.preventDefault();
const incrementOne = {
value: setNewValue(prevState => prevState + 1),
};
console.log(incrementOne);
};
Thank you in advance for your time and effort
Sincerely
/ Peter
In all your examples you are creating an object with a value property. You assume that is supposed to get it's value from calling set function returned by useState. However, the result of calling this function is updating the state, and re-rendering. The function itself doesn't return anything (undefined).
const incrementOne = {
value: setNewValue((prevState) => {
return {...prevState, newValue: newValue + 1}
})
};
You should call the setNewValue function when you want to update the value. You can calculate the new state using the previous one:
setNewValue(newValue + 1);
Or use a functional update to avoid depending on the state directly:
setNewValue(prevState => prevState + 1);
Note that the new value is only available after the component re-renders.
Example:
const { useState } = React;
const Form = (props) => {
const [newValue, setNewValue] = useState(0);
const submitHandler = () => {
setNewValue(prevState => prevState + 1);
};
const incrementOne = {
value: newValue,
};
console.log(incrementOne);
return (
<div>
<div>{newValue}</div>
<button onClick={submitHandler}>Submit</button>
</div>
);
}
ReactDOM.render(
<Form />,
root
)
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>

Resources