We use cookies to enhance your experience on the site
CodeWorlds

Advanced Patterns -- State Machines and Provider Pattern

Now that you know the basic React patterns, it's time for advanced techniques that will let you manage complex states and build scalable systems. In the space station -- it's like installing advanced control systems!

Provider Pattern -- Central distribution system

The Provider Pattern is an extension of the Context API. We create a dedicated provider that encapsulates both state and logic:

1const ThemeContext = createContext();
2
3function ThemeProvider({ children }) {
4  const [theme, setTheme] = useState('dark');
5
6  const toggleTheme = () => {
7    setTheme(prev => prev === 'dark' ? 'light' : 'dark');
8  };
9
10  const value = useMemo(() => ({
11    theme, toggleTheme
12  }), [theme]);
13
14  return (
15    <ThemeContext.Provider value={value}>
16      <div className={`app-${theme}`}>
17        {children}
18      </div>
19    </ThemeContext.Provider>
20  );
21}
22
23// Custom hook for consumption
24function useTheme() {
25  const context = useContext(ThemeContext);
26  if (!context) {
27    throw new Error('useTheme must be used within ThemeProvider');
28  }
29  return context;
30}

Provider composition

When we have many providers, composition is key:

1// Instead of nesting:
2function App() {
3  return (
4    <ThemeProvider>
5      <AuthProvider>
6        <NotificationProvider>
7          <AppContent />
8        </NotificationProvider>
9      </AuthProvider>
10    </ThemeProvider>
11  );
12}
13
14// We can use the compose pattern:
15function ComposeProviders({ providers, children }) {
16  return providers.reduceRight(
17    (acc, Provider) => <Provider>{acc}</Provider>,
18    children
19  );
20}
21
22function App() {
23  return (
24    <ComposeProviders
25      providers={[ThemeProvider, AuthProvider, NotificationProvider]}
26    >
27      <AppContent />
28    </ComposeProviders>
29  );
30}

State Machines -- Controlled state transitions

A state machine eliminates "impossible states" and guarantees that the application is always in one of the defined states:

1// States and transitions for the launch process
2const launchMachine = {
3  idle: {
4    START_PREFLIGHT: 'preflight'
5  },
6  preflight: {
7    CHECKS_PASSED: 'countdown',
8    CHECKS_FAILED: 'error'
9  },
10  countdown: {
11    COUNTDOWN_DONE: 'launching',
12    ABORT: 'aborted'
13  },
14  launching: {
15    LAUNCH_SUCCESS: 'inFlight',
16    LAUNCH_FAILURE: 'error'
17  },
18  inFlight: {},
19  error: {
20    RETRY: 'idle'
21  },
22  aborted: {
23    RETRY: 'idle'
24  }
25};
26
27function useMachine(machine, initialState) {
28  const [state, setState] = useState(initialState);
29
30  const send = useCallback((event) => {
31    setState(current => {
32      const nextState = machine[current]?.[event];
33      if (!nextState) {
34        console.warn(`No transition for ${event} in state ${current}`);
35        return current;
36      }
37      return nextState;
38    });
39  }, [machine]);
40
41  return [state, send];
42}
43
44// Usage
45function LaunchControl() {
46  const [state, send] = useMachine(launchMachine, 'idle');
47
48  return (
49    <div className="launch-control">
50      <h2>State: {state}</h2>
51      {state === 'idle' && (
52        <button onClick={() => send('START_PREFLIGHT')}>
53          Start procedures
54        </button>
55      )}
56      {state === 'preflight' && (
57        <>
58          <button onClick={() => send('CHECKS_PASSED')}>
59            Tests passed
60          </button>
61          <button onClick={() => send('CHECKS_FAILED')}>
62            Tests failed
63          </button>
64        </>
65      )}
66      {state === 'countdown' && (
67        <button onClick={() => send('ABORT')}>ABORT!</button>
68      )}
69      {state === 'error' && (
70        <button onClick={() => send('RETRY')}>Retry</button>
71      )}
72    </div>
73  );
74}

Why is a State Machine better than booleans?

Consider a typical scenario without a state machine. We have a component with several boolean states:

1// WITHOUT state machine -- "impossible states" are possible
2const [isLoading, setIsLoading] = useState(false);
3const [isError, setIsError] = useState(false);
4const [isSuccess, setIsSuccess] = useState(false);
5
6// What if isLoading and isError are BOTH true at the same time?
7// That's an "impossible state", but nothing prevents it!

With a state machine, this problem disappears -- the component is ALWAYS in exactly one state. There's no way for it to be in both the

loading
and
error
states simultaneously. It's like safety systems on a spaceship -- you can't be in flight mode and docking mode at the same time.

When to use specific patterns?

| Situation | Pattern | |-----------|---------| | Sharing data in the tree | Provider Pattern | | Multi-step processes | State Machines | | Form with validation | useForm hook + State Machine | | List with filtering | Custom hook + Presentational | | Component with variants | Composition + Props |

Summary

Provider Pattern and State Machines are advanced tools that solve real problems in large React applications. The Provider centralizes data and logic, eliminating "prop drilling". The State Machine organizes complex state flows, guaranteeing that the application never ends up in an undefined state. Both patterns can be combined -- for example, a Provider can expose a state machine to the entire component tree, giving every view access to the current mission state and transition functions.

Go to CodeWorlds