12
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"));

Edit redux-thunk-promise

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)

Avery235
  • 4,756
  • 12
  • 49
  • 83
  • 1
    IMO, update in the `componentDidUpdate` is the right way, because re-render and focus is both component's state and behavior, you cannot decouple them cleanly. I would even say you should move the toggle state into the component and just have some callback props for `onToggle` and `onClick`. – Tr1et May 28 '18 at 03:10
  • 1
    @Avery235 what makes you say "this is not a clean solution as the logic is specific to the onClick handler rather than a lifecycle event"? – duhaime Jun 01 '18 at 19:47

4 Answers4

2

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"));

Edit redux-thunk-promise


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.

Jivings
  • 22,834
  • 6
  • 60
  • 101
2

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.

karthik
  • 1,100
  • 9
  • 21
  • How is it not consistent with the HTML spec? What does the HTML spec say that is different from React? "if REACT added the option to focus on render" isn't it already possible with `autoFocus` prop? – Avery235 Jun 02 '18 at 06:25
  • For your 1st point. I havent specified that it is not consistent, it is consistent with HTML spec you might have mis read it, for the 2nd i provided the html spec (PFA link) where in the attributes section there will a documentation on autofocus. – karthik Jun 02 '18 at 06:47
  • How autoFocus is functional yet not functional? The best use case to describe this would be when a page has multiple fields where The autoFocus attribute will only work on the initial render cause the intended behaviour is only to move focus on first render, and this is consistent with the HTML spec for autofocus. – karthik Jun 02 '18 at 06:58
1

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:

  1. Dispatch async action toggleThunk, and wait for its resolution
  2. then dispatch synchronous action to update the state (new state data), and wait for its resolution (?)
  3. then focus() your ref

https://codesandbox.io/s/r57v8r39om

Edit redux-thunk-promise

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"));
Reed Dunkle
  • 3,408
  • 1
  • 18
  • 29
  • 1
    This is my original approach (see edit), I removed it because it assumes that `this.props.toggleThunk()` will only be resolved after the component re-renders. Why do you think this is a guaranteed behavior and is the behavior documented/referenced anywhere? – Avery235 May 28 '18 at 04:22
  • Good question, I'll see what I can find. – Reed Dunkle May 28 '18 at 04:35
  • @Avery235 Any news on my updates? I believe my original solution does guarantee the behavior after all. But the other solutions in my edit would also do the job. – Reed Dunkle Jun 01 '18 at 17:50
  • All your solutions seem to revolve around the assumption I mentioned in the above comment, which you did not address. – Avery235 Jun 02 '18 at 06:22
  • I think I did address it: **1** Dispatch async action and wait for its resolution, **2** `then` dispatch synchronous action to update the state (new state data that you need to check) and wait for its resolution, **3** `then` focus() your ref. What am I overlooking? – Reed Dunkle Jun 02 '18 at 22:27
  • `setState` is not synchronous. You should put the `.focus()` call in the callback of `setState`. Also, the actual use case uses another value to determine the disabled prop, which is stored in Redux for persistence, so duplicating it in local state violates single source of truth. – Avery235 Jun 03 '18 at 01:42
  • I meant my original Redux solution, not the component state solution – Reed Dunkle Jun 03 '18 at 03:54
  • I'm not sure how your original solution address the assumption. It only works if the assumption I mentioned in the first comment is true? – Avery235 Jun 03 '18 at 06:21
0

You can manage this with the prop and a ref. The ref will avoid the need to rerender the input (i.e. for autoFocus to work):

import React, { Component } 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 Component {

  componentDidUpdate(prevProps) {
    if (!this.props.disabled && prevProps.disabled) {
      this.ref.focus();
    }
  }

  render() {
    const { disabled } = this.props;
    return (
      <div>
        <input
          type="checkbox"
          checked={!disabled}
          onClick={() => {
            this.props.toggle();
          }}
        />
        <input
          disabled={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"));

Edit redux-thunk-promise

ic3b3rg
  • 14,629
  • 4
  • 30
  • 53
  • I need to store the state in redux (to persist it). Could duplicate it in component but that violates single source of truth. – Avery235 Jun 02 '18 at 06:15
  • Isn't it identical to my solution in OP? – Avery235 Jun 02 '18 at 06:34
  • yes, very similar - maybe I didn't understand your question - this is the correct pattern for handling the behavior - you can't set focus to the input until after it's been enabled, which doesn't happen until the render cycle after the action has propagated - make sense? – ic3b3rg Jun 02 '18 at 06:41
  • I was hoping for a better solution, where you have some way of designing the action dispatcher and the `onClick` handler such that you can call `.focus()` within `onClick` handler. I guess there's no such solution... – Avery235 Jun 02 '18 at 07:05
  • the `onClick` event is the correct place to dispatch the action and the`componentDidUpdate` function is the correct lifecycle moment to detect the state (i.e. store) change and set focus to the input. – ic3b3rg Jun 02 '18 at 07:12