Event-Driven Architectures for Full-Stack Devs

CITYJS / MEDELLÍN / 2024

TEMPORAL / REACT / FULL-STACK

I'm J.D. Nicholls 👋

Digital nomad 🎒 | Mentor 👨‍🏫 | Speaker 🗣️ | Developer 👨‍💻 | OSS Contributor 🍫 | Creator of @proyecto26 🧚

 

Founding Full-Stack Engineer 👷

BAXUS.co is the global marketplace to buy and trade the world's most collectible spirits 🥃

TEMPORAL FOR EVENT-DRIVEN ARCHITECTURES

 

SET UP

WORKERS

WORKFLOWS

ACTIVITIES

QUERIES AND SIGNALS

 

THANK GOODNESS I TOOK

THAT CODING WORKSHOP

Agenda

Agenda

01

Getting Started

02

Workers

03

Workflows

04

Activities

01

Set Up

Modern Application Architecture

Modern Application in Production

Modern Application in Reality

MODERN APPLICATIONS - MODERN PROBLEMS

Developers spend an inordinate amount of time coding and testing for complexity and inevitable failure

DURABLE EXECUTION

..is an abstraction that opens up an entirely new,
 and fundamentally better
development model

Temporal allows developers to deliver more business logic, faster. It eliminates complex plumbing code, so that developers can spend more time building features.

“The business was worried about getting the new capability done for Q4, but we did it in 1 day with Temporal.”

“I estimate we saved two engineers by adopting Temporal for our team of seven, which is significant.”

“There's no such thing as 100% reliability. But we're pretty close in terms of the two systems we have in flight with Temporal.”

"Temporal provides deep understanding of what's happening, at given time, for any message, and for every customer."

When failures happen, Temporal recovers processes where they left off or rolls them back correctly. The end result is fewer incidents and less downtime.

Temporal tracks application state for every execution, giving you insight into issues so you can more effectively debug code and understand application performance.

Orders & Bookings

Inventory & Logistics

Payment Gateway

Customer Onboarding

Customer Lifecycle

Subscription Lifecycle

Payment Processing

Risk & Fraud Analysis

Identity Verification

Operations & Data

CI/CD

Infra management

Infras provisioning

AI & ML

AI Inferences

Data pipeline & ETL

Model Training

Platform

Control Plane

Workflows (DSL)

Order management applications ensure successful completion of an order, like an e-commerce order or flight booking, and often span the end-to-end order lifecycle:

 

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

Workers

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

Workflows

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

Activities

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

05

Queries, Signals and Updates

DEMO / REACT / REMIXJS

TEMPORAL / NODEJS

NESTJS / NX / 🍭🍬🍫🍦

 

Of course Elon, Code here! 🎁

Resources

Keep Learning!

Let’s keep in touch!

 

Juan D. Nicholls

jd@baxus.co

¡Gracias!

 

Event-driven Architectures for Full-Stack Devs

By J.D Nicholls

Event-driven Architectures for Full-Stack Devs

Workshop of Event-Driven Architectures for Full-Stack Devs with Temporal and React: Join us for a hands-on workshop where you’ll learn how to build resilient and scalable full-stack applications using Temporal and React. We’ll dive into advanced workflow orchestration with Temporal’s Node.js SDK (TypeScript) and demonstrate how to seamlessly integrate these workflows into React applications using React Query. What You’ll Learn: • Advanced Workflow Orchestration with Temporal and Node.js: • Create and manage complex workflows to handle asynchronous processes and distributed transactions. • Define activities, signals, and efficiently manage state within your workflows. • Integration with React and React Query: • Incorporate Temporal workflows into your React applications for real-time state management. • Utilize React Query to fetch and handle workflow states from the frontend. • Building a Real-World Payment Processing System: • Implement a system that handles transactions, state updates, cancellations, and compensations. • Reflect transaction states in real time within your application. • Applying Event-Driven Design Patterns: • Leverage event-driven architecture principles to enhance application resilience and scalability. • Design systems that respond efficiently to events and gracefully handle errors.

  • 132