A variable from useState within a hook callback is closured - react-hooks

I use useKey to respond to Enter.
const [isFocused, setIsFocused] = useState(false);
useKey("Enter", ev => {
console.log("ev, isFocused -> ", ev, isFocused);
});
However, the isFocused value printed is always false even if it was changed.
How can one prevent a useState variable from being frozen to the initial value?

The useKey hook supports dependencies (4th param):
const useKey = (key: KeyFilter, fn: Handler = noop, opts: UseKeyOptions = {}, deps: DependencyList = [key])
Wrap the event handler with useCallback and set it's dependancy to be isFocused, and then pass the event handler as a dependancy to useKey. If isFocused changes, eventHandler changes, and useKey will update accordingly:
const eventHandler = useCallback(ev => {
console.log("ev, isFocused -> ", ev, isFocused);
}, [isFocused])
useKey("Enter", eventHandler, {}, [eventHandler]);

Related

React - Controlled input props are outdated in useCallback

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} />;
};

use reference value mounted with multiple useEffect in a component

const mounted = useRef(false)
is used in my functional component to check if the component is mounted or not. Here is the detail:
export default MyCom = () => {
const mounted = useRef(false)
useEffect(() => { //for one useEffect,
mounted.current = true;
//do something about states update
return () => mounted.current = false;
}, []);
useEffect(() => {. //here is 2nd useEffect which loads when [state1,state2] changes
mounted.current = true: //do the same thing about mounted here???
//do states update
return () => mounted.current = false; //clean up before return???
}, [state1, state2])
}
If there are multiple useEffect in the component, shall mounted.current=true be placed in every useEffect after the first one?

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

Why does my timer hook not update it's internal state?

For some reason my timer is not updating it's internal Timer State after I modify the input field. Here is the intial state of my page and State.
This is what my screen and state look like after I modify the input from 10 to 8 seconds. Notice that the Timer State does not update
Here is my code for the workout page:
function WorkoutPage(props: any) {
const DEFAULT_SECONDS_BETWEEN_REPS: number = 10
const [secondsBetweenRepsSetting, setSecondsBetweenRepsSetting] = useState(DEFAULT_SECONDS_BETWEEN_REPS)
const {secondsLeft, isRunning, start, stop} = useTimer({
duration: secondsBetweenRepsSetting,
onExpire: () => sayRandomExerciseName(),
onTick: () => handleTick(),
})
const onTimeBetweenRepsChange = (event: any) => {
const secondsBetweenRepsSettingString = event.target.value;
const secondsBetweenRepsSettingInt = parseInt(secondsBetweenRepsSettingString)
setSecondsBetweenRepsSetting(secondsBetweenRepsSettingInt)
}
return <React.Fragment>
<input type="number" name="secondsBetweenRepsSetting" value={secondsBetweenRepsSetting} onChange={onTimeBetweenRepsChange}/>
</React.Fragment>
}
Here is my useTimer Class:
import { useState } from 'react';
import Validate from "../utils/Validate";
import useInterval from "./useInterval";
export default function useTimer({ duration: timerDuration, onExpire, onTick}) {
const [secondsLeft, setSecondsLeft] = useState(timerDuration)
const [isRunning, setIsRunning] = useState(false)
function start() {
setIsRunning(true)
}
function stop() {
setIsRunning(false)
}
function handleExpire() {
Validate.onExpire(onExpire) && onExpire();
}
useInterval(() => {
const secondsMinusOne = secondsLeft - 1;
setSecondsLeft(secondsMinusOne)
if(secondsMinusOne <= 0) {
setSecondsLeft(timerDuration) // Reset timer automatically
handleExpire()
} else {
Validate.onTick(onTick) && onTick();
}
}, isRunning ? 1000 : null)
return {secondsLeft, isRunning, start, stop, }
}
My full codebase is here in case someone is interested: https://github.com/kamilski81/bdt-coach
Here's the sequence of events you expect:
User changes the input
The change handler fires and calls setSecondsBetweenRepsSetting with the new value
The component re-renders with the new value for secondsBetweenRepsSetting
useTimer is invoked with a duration property of the new value
The secondsLeft state in the useTimer hook changes to the new duration value <-- oops! this does not happen
Why doesn't this last item happen? Because within the useTimer implementation, the only place you use the duration is as the initial value of secondsLeft. Calling the hook a second time with a new duration value will not change the secondsLeft state, and this is by design.
My recommendation would be to include setSecondsLeft in the return value of the useTimer hook to give you a way to override the time left in the timer. You could then use setSecondsLeft directly in the input change handler:
const { secondsLeft, setSecondsLeft, isRunning, start, stop } = useTimer({
duration: secondsBetweenRepsSetting,
onExpire: () => sayRandomExerciseName(),
onTick: () => handleTick(),
});
const onTimeBetweenRepsChange = (event: any) => {
const secondsBetweenRepsSettingString = event.target.value;
const secondsBetweenRepsSettingInt = parseInt(
secondsBetweenRepsSettingString
);
setSecondsBetweenRepsSetting(secondsBetweenRepsSettingInt);
setSecondsLeft(secondsBetweenRepsSettingInt);
};

How would I build a Bootstrap React Modal using Scala Js React

How would I create a Modal component from the React Bootstrap library using the Scala.js React library?
The fact that a Modal has to change state (show = true/false) to show/hide the dialog made the solution non-trivial. I resolved this by wrapping it in a component that had a Boolean state that could be changed - and when it needs to be shown/hidden I change state with effects impure.
Another issue was that if the Modal has buttons that need to change the state their event handlers need access to this state somehow. I resolved this issue by giving users of the component access to the Backend of the component on creation.
Here is my implementation of the Modal:
class Modal(bs: BackendScope[Unit, Boolean], onHide: => Unit, children: Modal => Seq[ChildArg]) {
def render(show: Boolean) = {
val props = (new js.Object).asInstanceOf[Modal.Props]
props.show = show
props.onHide = () => {
dismiss()
onHide
}
Modal.component(props)(children(this): _*)
}
def dismiss() = {
bs.withEffectsImpure.setState(false)
}
}
object Modal {
#JSImport("react-bootstrap", "Modal")
#js.native
object RawComponent extends js.Object
#js.native
trait Props extends js.Object {
var show: Boolean = js.native
var onHide: js.Function0[Unit] = js.native
}
val component = JsComponent[Props, Children.Varargs, Null](RawComponent)
type Unmounted = Scala.Unmounted[Unit, Boolean, Modal]
def apply(onHide: => Unit = Unit)(children: Modal => Seq[ChildArg]): Unmounted = {
val component = ScalaComponent.builder[Unit]("Modal")
.initialState(true)
.backend(new Modal(_, onHide, children))
.renderBackend
.build
component()
}
}
And a Dialog object that uses it:
object Dialog {
object Response extends Enumeration {
type Response = Value
val OK, CANCEL = Value
}
import Response._
def prompt(title: String, body: String, okText: String): Future[Response] = {
// Add an element into which we render the dialog
val element = dom.document.body.appendChild(div(id := "dialog").render).asInstanceOf[Element]
// Create a promise of a return and a method to send it
val p = Promise[Response]
def respond(ret: Response) = {
// Remove the containing element and return the response
dom.document.body.removeChild(element)
p.success(ret)
}
Modal(respond(Response.CANCEL)) { modal =>
// Function to dismiss the dialog and respond
def quit(ret: Response) = {
modal.dismiss()
respond(ret)
}
// Create the components for our Modal
Seq(
ModalHeader(true,
ModalTitle(title)
),
ModalBody(body),
ModalFooter(
Button(variant = "secondary", onClick = () => { quit(Response.CANCEL) })("Cancel"),
Button(variant = "primary", onClick = () => { quit(Response.OK) })(okText)
)
)
}.renderIntoDOM(element).backend
p.future
}
}

Resources