React Hooks

One year later

βš›πŸŽ£

Hi! πŸ‘‹

I'm Dani

Software Engineer at

@d4nidev

delacruz.dev

Professional mentor for developers

πŸ“ Topics

  • Why I wasn't a Hooks early adopter
  • Insights, tips and tricks

What's wrong with Higher Order Components*?

πŸ€”

*Or with render props

// withPokemons.js
function withPokemons(Component) {
  return class WithPokemons extends React.Component {
    state = {
      pokemons: [],
      loading: true
    };
    componentDidMount() {
      this.fetchPokemons();
    }
    fetchPokemons = async id => {
      this.setState({ loading: true });

      const response = await fetch("https://pokeapi.co/api/v2/pokemon");
      const parsed = await response.json();

      this.setState({
        pokemons: parsed.results,
        loading: false
      });
    };
    render() {
      return <Component {...this.props} {...this.state} />;
    }
  };
}

Higher Order Components

// List.js
const List = ({ loading, pokemons }) => {
  if (loading) {
    return "Loading...";
  }

  return pokemons.map(pokemon => <div>{pokemon.name}</div>);
};

const ListWithPokemons = withPokemons(List);

Higher Order Components

// withPokemons.js
function withPokemons(Component) {
  return class WithPokemons extends React.Component {
    state = {
      pokemons: [],
      loading: true
    };
    componentDidMount() {
      this.fetchPokemons();
    }
    fetchPokemons = async () => {
      this.setState({ loading: true });

      const response = await fetch("https://pokeapi.co/api/v2/pokemon");
      const parsed = await response.json();

      this.setState({
        pokemons: parsed.results,
        loading: false
      });
    };
    render() {
      return <Component {...this.props} {...this.state} />;
    }
  };
}

Higher Order Components

export default withNotifications(
  withTheme(
    withAuth(
      withMouse(
        withScroll(
          withCurrentUser(
            withLocalStorage(
              withSettings(
                withPokemons(List)
              )
            )
          )
        )
      )
    )
  )
);

Clash name collisions

πŸ”₯Welcome to the wrapper hell πŸ”₯

// usePokemonsFetch.js
function usePokemonsFetch() {
  const [pokemons, setPokemons] = useState([]);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    setLoading(true);

    fetch("https://pokeapi.co/api/v2/pokemon")
      .then((response) => response.json())
      .then(parsed => setPokemons(parsed.results))
      .then(() => setLoading(false));
  }, []);
  
  return { pokemons, loading };
}

Hooks to the rescue

// PokemonList.js
function PokemonList() {
  const { pokemons, loading } = usePokemonFetch();
  
  if (loading) {
    return "Loading...";
  }

  return pokemons.map((pokemon) => <div>{pokemon.name}</div>);
}

With a custom hook

Should I stop using HOCs?

πŸ€”

  • HOCs might introduce an artificial abstraction
  • Could cause clash name collisions
  • Could cause performance issues
  • Using hooks helps you get rid of these problems
  • HOCs are better suited for cross-cutting concerns

πŸ₯‘ Take aways

What will happen to my tests?

πŸ™‹β€β™‚οΈ

show some code from enzyme

// accordion.js
import AccordionContents from './accordion-contents';

class Accordion extends React.Component {
  state = { openIndex: 0 }
  
  setOpenIndex = (openIndex) => this.setState({ openIndex })

  render() {
    const { openIndex } = this.state;
    const { items } = this.props;
    
    return (
      <div>
        {items.map((item, index) => (
          <AccordionContents open={index === openIndex}>
            <button onClick={() => this.setOpenIndex(index)}>
              {item.title}
            </button>
            {item.contents}
          </AccordionContents>
        ))}
      </div>
    )
  }
}

show some code from enzyme

test('setOpenIndex sets the open index state properly', () => {
  const wrapper = mount(<Accordion items={[]} />);
  expect(wrapper.state('openIndex')).toBe(0);
  wrapper.instance().setOpenIndex(1);
  expect(wrapper.state('openIndex')).toBe(1);
});

test('renders with the item contents', () => {
  const pokemons = [
    { title: 'Charmander', contents: 'Fire type lizard' },
    { title: 'Pikachu', contents: 'Electric type mouse'}
  ];
  const wrapper = mount(<Accordion items={pokemons} />)
  const child = wrapper.find('AccordionContents').props().childAt(1);
  expect(child).toBe(pokemons[0].contents);
});
// counter.js
class Counter extends React.Component {
  state = { count: 0 };
  
  increment = () => this.setState(({ count }) => ({ count: count + 1 }));
  
  render() {
    return (
      <button data-testid='button-increment' onClick={this.increment}>
        {this.state.count}
      </button>
    );
  }
};
// __tests__/counter.js
import { render, fireEvent } from '@testing-library/react'
import Counter from '../counter.js'

test('increments the counter', () => {
  const { getByTestId } = render(<Counter />);
  const button = getByTestId('button-increment');
  expect(button.textContent).toBe('0');
  fireEvent.click(button);
  expect(button.textContent).toBe('1');
});

Recommendation

// counter.js
function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => setCount((current) => current + 1)
  
  return (
    <button data-testid='button-increment' onClick={incrementCount}>
      {count}
    </button>
  );
};

πŸ₯‘ Take aways

  • Don't test implementation details
  • Refactor your tests before migrating components to React Hooks

Where are my shouldComponentUpdate() and PureComponent?

🌸

Short Answer:

Long answer: Measure!

In case you need it:

  • useMemo()
  • React.memo()
  • useCallback()

useMemo()

React.memo()

const Button = React.memo((props) => {
  // your component
}, arePropsEqual);

function arePropsEqual(prevProps, nextProps) {
  // compare props and return true if they are equal
}
// Will not change unless `a` or `b` changes
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

return <NestedComponent onSomething={memoizedCallback} />

useCallback()

Should I use one or many state variables?

πŸ€·πŸ»β€β™‚οΈ

useState()

πŸ›Ή

function MyComponent() {
  const [name, setName] = React.useState('');
    
  return (
    <>
      <div>{name}</div>
      <input onChange={(ev) => setName(ev.target.value)} />
    </>
  );
}

In a nutshell

const [age, setAge] = useState(42);
const [name, setName] = useState('Mike Myers');
const [doctor, setDoctor] = useState({ name: 'Mike', surname: 'Myers', age: 42 });

State can be as complex as you need

class Card extends React.Component {
  state = { left: 0, top: 0, width: 100, height: 100 };

  // ...

  this.setState({ width: newWidth, height: newHeight });
}

with this.setState()

function Card() {
  const [measures, setMeasures] = useState({ 
    left: 0, 
    top: 0, 
    width: 100, 
    height: 100 
  });

  // ...

  // Use object spread "...state" ensures you don't lose width and height
  setMeasures((current) => ({ ...current, width: newWidth, height: newHeight }));
}

with useState()

Keep together things that should change together

const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });

// ...
setPosition({ left: e.pageX, top: e.pageY });

Do I need Redux?

TL;DR You Might Not

But not because of Hooks

You probably didn't need it earlier, neither

🀭

useContext()

πŸ›Ή

Example: Global Notifications

// Note: implementation is very simplified
const NotificationsContext = React.createContext({
  onError: noop
  onSuccess: noop
  onLoading: noop
});

const Notifications = ({ children }) => {
  const [notification, setNotification] = useState({ type: 'none', message: '' });

  const onError = (message) => setNotification({ type: 'error', message });
  const onSuccess = (message) => setNotification({ type: 'success', message });
  const onLoading = (message) => setNotification({ type: 'loading', message });

  return (
    <>
      <Success open={notification.type === 'success'} message={notification.message} />
      <Error open={notification.type === 'error'} message={notification.message} />
      <Loding visible={notification.type === 'loading'} />
      <NotificationsContext.Provider value={{ onError, onSuccess, onLoading }}>
        {children}
      </NotificationsContext.Provider>
    </>
  );
};

Example: Global Notifications

// app.js
const App = () => (
  <Notifications>
    // ... your app's component tree
  </Notifications>
);
// some component in your tree
const { onError, onSuccess, onLoading } = useContext(NotificationsContext);

onLoading(true);

try {
  await fetch(myApiEndpoint)
  onSuccess('Success!');
} catch(err) {
  onError(e.message);  
} finally {
  onLoading(false);
}

useReducer()

πŸ›Ή

<FormComponent
  onSubmit={handleSubmit}
  onSubmitComplete={handleSubmitComplete}
  onError={handleError}
  onFetchStart={handleFetchStart}
  onFetchEnd={handleFetchComplete}
/>

Convenient for complex state mutations

function reducer(state, action) {
  switch (action.type) {
    case "ready":
      return { ...state, notification: "none" };
    case "submit_start":
      return {
        ...state,
        notification: "loading",
        loading: true,
        message: "Submitting..."
      };
    case "submit_complete":
      return {
        ...state,
        notification: "success",
        loading: false,
        message: "Saved!"
      };
  
    // ...
  }
}

Extract state mutation logic

const [state, dispatch] = useReducer(reducer, initialState);

<FormComponent dispatch={dispatch} {...state} />

Simplify your component

useReducer

useContext

You might need Redux:

  • Transaction telemetry
  • Time travel debugging
  • Single source of truth for application state
  • Deep props diffing

πŸ₯‘ Take aways

  • React Hooks don't fully replace Redux
  • You might need Redux just if your application is a complex state machine
  • React is already a state management library

πŸ“š Resources

Take your time

Thinking in React Hooks

πŸ€“ Advice

Ask me anything

πŸ—£

Thank you!

And stay at home!

πŸ™‡πŸ»β€β™‚οΈ

React Hooks: One year later

By Daniel de la Cruz Calvo

React Hooks: One year later

A talk about my experience with React Hooks, a year after I started using it. The talk is less focused on explaining how React or React Hooks work, and more on insights, tips and tricks based on what I've found while doing this transition from classes to hooks.

  • 334