I have a Button that stays disabled according to mapped state variables. It works when the page is loaded, but after processing, the state changes but the Button stays enabled.
The state should transition like this
loading: false -> button disabled: true
when button is clicked:
loading: true -> button disabled: true
when processing finishes:
loading: false -> button disabled: false
The loading state is changed, however the disabled attribute just changes for the first time.
Page.jsx (just some snippets for simplicity)
const [disabled, setDisabled] = useState(true);
const { loading } = useSelector(state => state.spreadsheet);
const importData = () => {
importOperations.createRows(rows, dispatch);
};
return (
<>
<Button
variant="contained"
color="primary"
onClick={importData}
className={classes.spacing}
disabled={disabled || loading}
>
Import
</Button>
</>
);
importOperations.js
export const createRows = async (rows, dispatch) => {
dispatch(importActions.setLoading(true));
// ......
dispatch(importActions.setLoading(false)); // this step is correctly executed
};
importReducer.js
export const INITIAL_STATE = {
messagesLog: [],
loading: false
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case spreadsheetActions.SET_LOADING:
return { ...state, loading: action.payload };
default:
return state;
}
};
Do you have any suggestion on why the button doesn't change back to disabled?
I would have commented and asked you to clarify the following first, but it won't let me comment, so I have to ask you here:
I think there may be a problem in the logic that you mentioned. But I could be wrong, so I am commenting here first before trying to answer your question.
You said you want the following but that won't work because, if loading is false, button disabled is true. If the button is disabled, you can't click on it.
You said when the button is clicked, you want loading to be true. That is fine, but you want button disabled to be false?? Would you want people to click on the button when it is loading?
loading: false -> button disabled: true
when button is clicked:
loading: true -> button disabled: false
when processing finishes:
loading: false -> button disabled: true
Regardless, I have put together some helpful code below to match what you asked. I'll be happy to assist further once you verify if the logic you mentioned is correct.
Can you try the following. it is the best I could put together to mimic your code somewhat
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [disabledState, setDisabledState] = useState(false);
const [loading, setLoading] = useState(false);
// sets loading to true when clicked, and
const importData = () => {
setLoading((loading) => !loading);
setTimeout(() => {
setLoading((loading) => !loading);
}, 1000);
};
// set loading to false initially when component mounts
useEffect(()=> {
setLoading(true)
},[])
// I would set disabled to true when loading else false
// but I have matched it to watch you mentioned in your post below
// you may changed your it accordingly to your needs here
useEffect(() => {
loading ? setDisabledState(false) : setDisabledState(true);
}, [loading]);
return (
<>
<span>{`${loading}`}</span>
<button
variant="contained"
color="primary"
onClick={() => importData()}
disabled={disabledState}
>
Import
</button>
</>
);
}
Here is a link to the CodeSandbox for the above: https://codesandbox.io/s/twilight-hill-urv8o?file=/src/App.js:0-1055
Related
I have (what seems to be) a very peculiar situation where I seem to be getting extra events emitted based on my Redux state.
I have narrowed the behavior down to whether or not I make a successful request to my /users endpoint and retrieve a list of users which is then stored in Redux.
If the commented code is not active (as it is currently shown), I am able to successfully render the modal(s) reliably and step between states.
If the commented code is active, the (which is what is behind the as well) emits an onDismiss call immediately. This has the result of closing the modal immediately.
If the commented code is active, but the response from the thunk is a 401 and the user data is not loaded (i.e., the state of the user key in redux is a failure, not success, then the modal works -- though of course, there are no users to select.
I have confirmed this behavior is consistent no matter where I seem to make this fetch request (initially it was in the App.tsx to be called immediately. I also tried it in an intermediate component).
Question(s):
Can you explain why I might be getting different behavior in my click handlers based on what is in my state?
Is there something I'm missing and I'm conflating my Redux state with the actual behavior?
I know I can solve this by adding a event.stopPropagation() call in strategic places (e.g., on the first button that opens the <ConfirmationBox> and then again on the button in the <ConfirmationBox> that transitions to the SelectUser modal), but are there other solutions?
//pinFlow.tsx
type States =
| { state: 'Confirm' }
| { state: 'SelectUser' }
| { state: 'SubmitPin'; user: User };
export function pinFlow<T extends ConfirmationBoxProps>(
ConfirmationBox: React.FC<T>,
authorization: Authorization,
) {
const [state, setState] = React.useState<States>({ state: 'Confirm' });
// const dispatch=useDispatch();
// initialize users
// const users = useSelector((state: InitialState) => state.pinAuth.users);
// const fetchUsers = useCallback(() => {
// dispatch(fetchUsersThunk());
// }, [dispatch]);
// useEffect(() => {
// if (users.state === RemoteDataState.NotStarted) {
// fetchUsers();
// }
// }, [fetchUsers, users.state]);
return (props: T) => {
const users = useSelector((state: InitialState) =>
mapRemoteData(state.pinAuth.users, users =>
users.filter(user => user.authorizations.includes(authorization)),
),
);
switch (state.state) {
case 'Confirm': {
return (
<ConfirmationBox
{...props}
onSubmit={(_event: React.MouseEvent) => {
setState({ state: 'SelectUser' });
}}
/>
);
}
case 'SelectUser': {
return (
<Modal
title={'PIN Required'}
canClickOutsideToDismiss={true}
onDismiss={() => {
setState({ state: 'Confirm' });
}}
>
<p className={style.selectProfileText}>Select your profile:</p>
<pre>
<code>{JSON.stringify(users, null, 4)}</code>
</pre>
{/*
<UserList users={users.data} /> */}
</Modal>
);
}
default: {
return <Modal title="others">all others</Modal>;
}
}
};
}
The code is used in another component like so:
function Comp(){
const [selected, setSelected] = useState();
const [mode, setMode] = useState();
const ConfirmationModal =
protected
? pinFlow(MenuItemModal, permission)
: MenuItemModal;
return(
<ConfirmationModal
item={selected}
mode={mode}
disabled={availability.state === RemoteDataState.Loading}
errorMessage={tryGetError(availability)}
onCancel={() => {
setMode(undefined);
dispatch(resetAvailability());
}}
onSubmit={(accessToken: string) => {
dispatch(findAction(selected, mode, accessToken));
}}
/>
)
}
After the first render, the useReducer hook doesn't react to changes in its initialArg (second positional) argument. It makes it hard to properly sync it with an external value, without having to rely on an extra cycle by dispatching a reset action inside a useEffect hook.
I built a minimal example. It's a simple, formik-like, form provider. Here's what it looks like:
// App.js
const users = {
1: {
firstName: 'Paul',
lastName: 'Atreides',
},
2: {
firstName: 'Duncan',
lastName: 'Idaho',
},
};
const App = () => {
const [id, setId] = useState(1);
return (
<>
<div>Pick User</div>
<button onClick={() => { setId(1); }} type="button">User 1</button>
<button onClick={() => { setId(2); }} type="button">User 2</button>
<FormProvider initialValues={users[id]}>
<Editor />
</FormProvider>
</>
);
};
// FormProvider.js
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
default:
throw new Error();
}
};
const FormProvider = ({ children, initialValues }) => {
const [values, dispatch] = useReducer(reducer, initialValues);
const handleChange = useCallback((evt) => {
dispatch({
field: evt.target.name,
type: 'UPDATE_FIELD',
value: evt.target.value,
});
}, []);
return (
<FormContext.Provider value={{ handleChange, values }}>
{children}
</FormContext.Provider>
);
};
// Editor.js
const Editor = () => {
const { handleChange, values } = useContext(FormContext);
return (
<>
<div>First name:</div>
<input
name="firstName"
onChange={handleChange}
value={values.firstName}
/>
<div>First name:</div>
<input
name="lastName"
onChange={handleChange}
value={values.lastName}
/>
</>
);
};
If you open the demo and click on the User 2 button, you'll notice that nothing happens. It's not surprising since we know that the useReducer hook gets initialised once using the provided initialArg argument and never reads its value again.
What I expect is the useReducer state to reflect the new initialArg prop, i.e. I want to see "Duncan" in the First name input after clicking on the User 2 button.
From my point of vue, I can see two options:
1. Passing a key prop to the FormProvider component.
// App.js
const App = () => {
// ...
return (
<>
{/* ... */}
<FormProvider key={id} initialValues={users[id]}>
<Editor />
</FormProvider>
</>
);
};
This will indeed fix the problem by destroying and re-creating the FormProvider component (and its children) every time the id changes. But it feels like a hack to me. Plus, it seems inefficient to rebuild that entire part of the tree (which is substantial in the real application) just to get that input values updated. However, this seems to be a common fix for such problems.
2. Dispatch a RESET action whenever initialValues changes
// FormProvider.js
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
case 'RESET':
return action.values;
default:
throw new Error();
}
};
const FormProvider = ({ children, initialValues }) => {
// ...
const isFirstRenderRef = useRef(true);
useEffect(() => {
if (!isFirstRenderRef.current) {
dispatch({
type: 'RESET',
values: initialValues,
});
}
}, [initialValues]);
useEffect(() => {
isFirstRenderRef.current = false;
}, []);
// ...
};
This will work as well, but, because it's happening inside a useEffect hook, it will require an extra cycle. It means that there'll be a moment where the form will contain stale values. If the user types at that moment, it could cause a race condition.
3. Idea
I read in this article by Mark Erikson that:
Function components may call setSomeState() directly while rendering, as long as it's done conditionally and isn't going to execute every time this component renders. [...] If a function component queues a state update while rendering, React will immediately apply the state update and synchronously re-render that one component before moving onwards.
So it seems that I should be able to call dispatch({ type: RESET, values: initialValues }); directly from the body of the function, under the condition that initialValues did change (I'd use a ref to keep track of its previous value). This should result in the state being updated in just one cycle. However, I couldn't get this to work.
——
What do you think is best between option 1, 2 and (3). Any advice/guidance on how I should address this problem?
I am trying to prevent a button being clicked consecutively in my project and only allow a single click. I would also like it to only be a single click and not allow a double click if that is possible?
To do this I would like to add a time out of maybe 5 seconds before the button can be pressed again but I'm not sure how to do this. The button is a link to redirect the user back to the homepage.
Is the a way to set the button on a timer when clicked?
<Button id="back-btn" variant="link" className="btn btn-link" onClick={props.goBack} alt="homepage">
Homepage
</Button>
Any ideas?
Cheers
R
basically you need to use a disabled state with a timer.
check this codepen: https://codepen.io/hasanagh/pen/MWaLxVK
state = {
disabled: false,
};
handleButtonClicked = () => {
//going back logic
this.setState({
disabled: true,
});
setTimeout(() => {
this.setState(() => ({
disabled: false,
}));
}, 5000);
};
render() {
const { disabled } = this.state;
return (
<button
onClick={this.handleButtonClicked}
disabled={disabled}
>
Button to be disabled
</button>
);
}
Also, not sure why you need it to be 5 sec if this is related to a certain event better bind to event than time.
It's probably most re-useable to make your button component. You could handle the onClick event to set a disabled state, then start a timer to set it back to false. Example:
const DebouncedButton = ({ as = button, delay, onClick, ...props }) => {
const [isDisabled, setDisabled] = useState(false);
useEffect(() => {
if (!isDisabled) {
// timeout elapsed, nothing to do
return;
}
// isDisabled was changed to true, set back to false after `delay`
const handle = setTimeout(() => {
setDisabled(false);
}, delay);
return () => clearTimeout(handle);
}, [isDisabled, delay]);
const handleClick = (e) => {
if (isDisabled) {
return;
}
setDisabled(true);
return onClick(e);
};
const Component = as;
return <Component {...props} disabled={isDisabled} onClick={handleClick} />;
};
You would use this component just like you'd use a button, except that you pass it a delay which is the amount of time in milliseconds it should be disabled after clicking. The as prop lets you pass the component which is used for the button itself, defaulting to <button>.
<DebouncedButton
as={Button}
delay={5000}
id="back-btn"
variant="link"
className="btn btn-link"
onClick={() => console.log('click!')}
alt="homepage"
/>
Currently it sets the disabled property of the button to true, but if you don't want the visual, just remove disabled={isDisabled} from the component.
I am using useReducer to control my 2 inputs:
const [noteInput, setNoteInput] = useReducer(
(state, newState) => ({ ...state, ...newState }),
{
title: '',
content: ''
}
);
After onClick on button i want both inputs to be cleared. How can i do that after useReducer?
You can update your state to empty using setState OR You can dispatch other actions for updating that state to the empty string.
https://redux.js.org/basics/reducers#reducers
If you are just looking to use useReducer to clear your form you can use dispatch to do so. Let's use this example button component as an example.
//Component
<Button onClick={() => {dispatch({ type: "CLEAR_FORM"})}}>Submit</Button>
After clicking the button "CLEAR_FORM" is dispatched to the reducer.
//Form Initial State & Reducer switch statement
export const initialState={
username:"",
password:""
}
export const reducer = (state, action) => {
switch(action.type){
case: "CLEAR_FORM":
return {
username:"",
password:"",
}
default:
return state
}
}
When the reducer gets the { type: "LOG_OUT" }, in this case, it resets the username and password fields to an empty string.
https://reactjs.org/docs/hooks-reference.html#usereducer
You can pass a third argument to React.useReducer() called init. This is called lazy initialization and is a quick way to revert to the initial state.
https://reactjs.org/docs/hooks-reference.html#lazy-initialization
If you have a Button, you only need to alter onSubmit={handleSubmit} and have the function below and things will be all good. Just remember not to set defaultValue of any TextField or what you are using, just set the value only
const handleReset = (event) => {
event.preventDefault();
Object.keys(formInput).forEach((inputKey) => {
setFormInput({ [inputKey]: '' });
});
};
I am creating a game in React which also uses the Google Firestore in order to keep track of game data.
In the game, the user will hit "Start" and be connected to a Google Firebase collection, which starts listening for changes to the database. When something in the database changes, the React component state will change.
The problem I am having is that is seems that the version of state my listener's callback function uses is out of date. The listener is set in response to the player hitting the start button, and the component's state at that time seems to be cached.
The problematic component looks like this:
import React, { useEffect, useState } from "react";
function Counter() {
const [state, setFullState] = useState({
count: 0,
clicks: 0,
started: false
});
const setState = update => {
setFullState(prevState => {
return {
...prevState,
...update
};
});
};
const handleStart = () => {
const increment = () => {
const { clicks } = state;
setState({ clicks: clicks + 1 });
};
setState({
started: true
});
document.addEventListener("click", increment);
};
const { clicks, count, started } = state;
if (!started) {
return (
<div>
<button onClick={handleStart}>START</button>
</div>
);
}
return (
<div>
<h2>Hooks</h2>
<p>Count: {count}</p>
<p>Clicks: {clicks}</p>
<p>
<button
onClick={() => {
setState({ count: count + 1 });
}}
>
More
</button>
</p>
</div>
);
}
export default Counter;
I created a sandbox to demonstrate the problem. When you click on the button, the "Clicks" number does not increase.
What is a good way to make a function reference the component's state at the time the function is called, not at the time the function is created?
Using useRef you can keep a snapshot of the state visible in the now stale increment callback, and keep it in sync.
const currentState = React.useRef(state)
currentState.current = state
then
const handleStart = () => {
const increment = () => {
const { clicks } = currentState.current;
setState({ clicks: clicks + 1 });
};
setState({
started: true
});
document.addEventListener("click", increment);
};
This should answer your question, but your problem can be solved in a better way than this.