import React from "react";
import { render } from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider, connect } from "react-redux";
import thunk from "redux-thunk";
const disabled = (state = true, action) => {
return action.type === "TOGGLE" ? !state : state;
};
class Button extends React.Component {
componentDidUpdate(prevProps) {
if (prevProps.disabled !== this.props.disabled && !this.props.disabled) {
// this.ref.focus(); // uncomment this to see the desired effect
}
}
render() {
const { props } = this;
console.log("rendering", props.value);
return (
<div>
<input
type="checkbox"
onClick={() => {
props.toggle();
this.ref.focus(); // doesn't work
}}
/>
<input
disabled={props.disabled}
ref={ref => {
this.ref = ref;
}}
/>
</div>
);
}
}
const toggle = () => ({
type: "TOGGLE"
});
const A = connect(state => ({ disabled: state }), { toggle })(Button);
const App = () => (
<Provider store={createStore(disabled, applyMiddleware(thunk))}>
<A />
</Provider>
);
render(<App />, document.getElementById("root"));
I want to focus the input
when the checkbox is checked.
However, this.ref.focus()
must be called only after the component re-renders with props.disabled === false
, as an input
with disabled
prop cannot be focused.
If I do the logic in componentDidUpdate
, I'm able to achieve what I want. But this is not a clean solution as the logic is specific to the onClick
handler rather than a lifecycle event.
Is there any other way to accomplish this? (preferably with a working codesandbox example)
I think the best thing to do is not rely on refs use state to manage the focus.
This solution instead uses the autoFocus
prop on the input, and modifies it when the state of the checkbox changes.
import React from "react";
import { render } from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider, connect } from "react-redux";
import thunk from "redux-thunk";
const disabled = (state = true, action) => {
return action.type === "TOGGLE" ? !state : state;
};
class Button extends React.Component {
state = {
checked: false,
focus: false
};
componentDidUpdate(prevProps, prevState) {
if (prevState.checked !== this.state.checked) {
this.props.toggle();
this.setState({
focus: this.state.checked
});
}
}
render() {
const { props } = this;
const { checked, focus } = this.state;
console.log("rendering", props.value, checked);
return (
<div>
<input
type="checkbox"
checked={checked}
onClick={({ target }) => {
this.setState({ checked: target.checked });
}}
/>
<input key={`input_${checked}`} autoFocus={focus} />
</div>
);
}
}
const toggle = () => ({
type: "TOGGLE"
});
const A = connect(state => ({ disabled: state }), { toggle })(Button);
const App = () => (
<Provider store={createStore(disabled, applyMiddleware(thunk))}>
<A />
</Provider>
);
render(<App />, document.getElementById("root"));
I'm not sure why, but changing the autoFocus
prop when the component was previously disabled doesn't trigger the input to be re-rendered. So I've also added a key to the input to force it.
This is an hypothetical situation and an open issue in REACT(at the same time NOT) since it is consistent with the HTML spec for autofocus (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes#autofocus). Focus is one of those things that is really tricky to do decoratively because it's part of a shared global state. If 2 unrelated components declare that they should be focused in a single render pass, who is right? So REACT give you the hooks to manage that state yourself but it won't do it for you (thus where the work around like the one your are using came).
But It would be great if REACT added the option to focus on render (could be just autoFocusOnRender), and just have the docs warn people of the behavior if multiple things call for focus at once. Ideally this wouldn't happen because an app with good UX would have specific conditions for calling autoFocusOnRender on different inputs.
I would Suggest what you have done is the best way of doing it :). Hope we get an enhancement for this in REACT.
I think that you can have confidence that the updated Redux state
data is there before you perform your focus()
call, because of the data flow:
toggleThunk
, and wait for its resolutionthen
dispatch synchronous action to update the state
(new state
data), and wait for its resolution (?)then
focus()
your refhttps://codesandbox.io/s/r57v8r39om
Note that in your OP, your toggle()
action creator is not a thunk. Also, it's a good rule to enforce that your thunks return a Promise so that you can control data flow in the way you're describing.
import React from "react";
import { render } from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider, connect } from "react-redux";
import thunk from "redux-thunk";
const disabled = (state = true, action) => {
return action.type === "TOGGLE" ? !state : state;
};
class Button extends React.Component {
textInput = React.createRef();
handleClick = () => {
const { toggleThunk } = this.props;
toggleThunk().then(() => {
this.textInput.current.focus();
});
};
render() {
const { disabled, value } = this.props;
return (
<div>
<input type="checkbox" onClick={this.handleClick} />
<input disabled={disabled} ref={this.textInput} />
</div>
);
}
}
// Action
const toggle = () => ({
type: "TOGGLE"
});
// Thunk
const toggleThunk = () => dispatch => {
// Do your async call().then...
return Promise.resolve().then(() => dispatch(toggle()));
};
const A = connect(state => ({ disabled: state }), { toggleThunk })(Button);
const App = () => (
<Provider store={createStore(disabled, applyMiddleware(thunk))}>
<A />
</Provider>
);
render(<App />, document.getElementById("root"));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With