Real-time & Hooks

SEATTLE / SÃO PAULO / PANAMÁ

BOGOTÁ / MEDELLÍN

 

     there,

I'm J.D. Nicholls 👋

- Frontend Developer at S4N 👷

- Open Source Contributor 👨‍💻

- Developer who loves UX 💫

- Game developer (Hobby) 🎮

- Chocolate lover 🍫

- Creator of Proyecto26 🧚

Be the best version of yourself! 💪

HOOKS IN REACT

 

LIFECYCLE

STATE MANAGEMENT

EXTEND BEHAVIOR

REAL-TIME CONNECTIONS

 

Agenda

Agenda

01

Component State

02

Hooks & Actions

03

Testing Hooks

04

Real-time

01

Component State

function CustomListContainer() {
  const [error, setError] = React.useState();
  const [items, setItems] = React.useState([]);
  
  useEffect(() => {
    getItems()
      .then(setItems)
      .catch(setError)
  }, [])
  
  return error ? <ErrorContainer error={error} /> : (
    <IonList>
      items.map(item => (
      	<IonItem>
          <IonLabel>{ item.name }</IonLabel>
        </IonItem>
      ))
    </IonList>
  )
}

Basic Component state

composable stateful interfaces 💡

 

Benefits

- Reusable components

- Composition over inheritance

- Follow the kiss principle

- It's possible to simulate behaviors of class components with hooks

// Try to avoid this pattern if possible.
const [ignored, forceUpdate] = useReducer(x => x + 1, 0)

- Do Hooks cover all use cases for classes? No

function SignInScreen() {
  // useState is not the right way for complex state logic 
  const [userName, setUserName] = React.useState(getName());
  const [password, setPassword] = React.useState('');
  const [error, setError] = React.useState(null);
  const [loading, setLoading] = React.useState(false);
  
  // Using previous state
  const onLogin = async () => {
    try {
      setLoading(true)
      await authenticate(userName, password)
      ...
    } catch (error) {
      setError(error)
    } finally {
      setLoading(false)
    }
  }
  
  // We can't test this code directly
  const onFunA = () => ...
  
  // Creating a new function on every render
  const onFunB = () => ...
   
  return ...
}

Developers

one of the bad parts

function SignInScreen () {
  const [state, dispatch] = useReducer<SignInReducer>(reducer, {
    error: null,
    loading: false,
    userName: '',
    ...
  })
}
import { Reducer } from 'react'

type OptionError = Error | null

interface SignInScreenState {
  error: OptionError,
  loading?: boolean,
  userName?: string,
}

export enum Action {
  SetError,
  SetLoading,
  SetUsername
}

type SignInScreenAction =
 | { type: Action.SetError, value: OptionError }
 | { type: Action.SetLoading, value: boolean }
 | { type: Action.SetUsername, value: string }

type SignInReducer =
  Reducer<SignInScreenState, SignInScreenAction>
const reducer = (
  state: SignInScreenState,
  action: SignInScreenAction
): SignInScreenState => {
  switch (action.type) {
    case ACTIONS.SET_ERROR:
      return {
        ...state,
        error: action.value
      }
    case ACTIONS.SET_LOADING:
      return {
        ...state,
        loading: action.value
      }
    case ACTIONS.SET_USERNAME:
      return {
        ...state,
        userName: action.value
      }
    ...
    default:
      return state
  }
}

useReducer

more control for

performance

02

Hooks &

Actions

Hooks

Reusable Logic

A way to reuse stateful logic 💡

function useAuth() {
  // Use stores with React Context API, etc
  const [state, dispatch] = useStore();
  // memoized function for performance
  const onSignIn = useCallback(async (
    document: string,
    password: string,
  ) => {
    const { data: { access_token: accessToken } } = await axios.post(
      authEndpoint,
      { document, password }
    );
    dispatch({ type: Action.LoadToken, value: accessToken });
  }, [dispatch]);
  
  return {
    state,
    onSignIn
  };
}
function useAuth() {
  ...
  const onLoadUser = useCallback(async () => {
    const { data: user } = await axios.get(`${userEndpoint}/me`, {
      headers: { 'Authorization': `Bearer ${state.token}` }
    });
    dispatch({
      type: Action.LoadUser,
      value: user
    });
  }, [state.token, dispatch]);
  
  return {
    ...,
    onLoadUser
  }
}

Memoized callback

prevent renders

Only changes if one of the dependencies has changed 💡

 

Benefits

- Single source of truth
- No code duplication

- All actions are visible

- No more need the deep component tree nesting reusing state logic (HOCs can be used for specific cases like adding behaviors, injecting stores via props, etc)

- Many components can react to same action
- Test components and hooks separately

03

Testing Hooks

import { act, renderHook } from '@testing-library/react-hooks'

import { useDictionary } from '..'

describe('useDictionary hook', () => {
  it('should handle onUpdateValue action', () => {
    const initialState = {
      boolean: false,
    }

    const { result } = renderHook(() => useDictionary(initialState))

    act(() => {
      result.current.onUpdateValue('boolean', true)
    })
    expect(result.current.state.boolean).toBeTruthy()
  })
})

Testing

what really matters

Encourage good testing practices 💡

it('should handle useEffect hook', async () => {
  const spyInitialize = jest.spyOn(HubConnection, 'initialize').mockResolvedValueOnce()
  const spyClose = jest.spyOn(HubConnection, 'close').mockReturnValueOnce()
  const { result, waitForNextUpdate, unmount } = renderHook(() => useMyHook())

  await waitForNextUpdate()

  expect(spyInitialize).toHaveBeenCalled()
  expect(result.current.initialized).toBeTruthy()

  unmount()

  expect(spyClose).toHaveBeenCalled()
})

Reusable

Isolated units

Just use the hook directly and assert the results 💡

04

Real-time

import { useState, useEffect, useCallback } from 'react'
import io, { Socket } from 'socket.io-client'
import { WS_DOMAIN } from '../../contants'

export function useSocket () {
  const [socket, setSocket] = useState<typeof Socket | null>(null)

  useEffect(function () {
    const newSocket = io.connect(WS_DOMAIN)
    setSocket(newSocket)

    return () => {
      newSocket.removeAllListeners()
      newSocket.close()
    }
  }, [])

  const onSendMessage = useCallback((eventName: string, data: any) => {
    if(socket) socket.emit(eventName, data)
  }, [socket])

  return {
    socket,
    onSendMessage
  }
}

WebSockets

Connect and Close

import { useState, useEffect, useCallback } from 'react'
import io, { Socket } from 'socket.io-client'
import { WS_DOMAIN } from '../../contants'
import { useAuth } from '../useAuth'

export function useSocketWithAuth () {
  const { state } = useAuth()
  const [socket, setSocket] = useState<typeof Socket | null>(null)

  useEffect(function () {
    if (!state.token) return
    const newSocket = io.connect(WS_DOMAIN, {
      query: { token: state.token }
    })
    setSocket(newSocket)

    return () => {
      newSocket.removeAllListeners()
      newSocket.close()
    }
  }, [state.token])
  
  return {
    socket
  }
}

Authentication

Validate users

DEMO / REACT / HOOKS

REAL-TIME / GAMES

 

Of course Elon, Code here! 🎁

Resources

Keep Learning!

- Additional Hooks: specific edge cases
- React Hooks: Why are they neccesary?

- Adios Redux: using React hooks and Context effectively​
- Functional-Light JavaScript: Pragmatic, balanced FP in JavaScript
- Composing Software: The Book

Assets of this presentation are part of S4N Styleguide

Let’s keep in touch!

 

Juan D. Nicholls

juannicholls@s4n.co

¡Gracias!

 

Real-time & Hooks with React

By Juan David Nicholls

Real-time & Hooks with React

Component State, Hooks & Actions, Testing Hooks and Real-time

  • 2,882