Skip to content

wzychla/Guess6

Repository files navigation

Guess6

This is a simple React/Typescript playground to study basic React features. The app is just a game which allows the user to guess a 6-letter word from a dictionary.

guess6

Background

The app was written during a rainy saturday afternoon, in January of 2022. One of my daugthers came with a mobile version of this and a complaint about the mobile app showing ads too often. My natural reaction was "hey, this looks simple, I wonder how much time does it take to implement this in React".

Turns out, about an hour, at least for me. I even blogged about it.

Some time later, I've decided to spend yet another hour to polish the code a little bit, fix some issues and take a more general approach where some basic React features could be packed together to show various ways of doing things (like parent-child communication).

Points of interests

The main component, App, basically renders the Keyboard and a list of candidate words, where each candidate is rendered as an instance of the WordMatch component (responsible for correct coloring of letters).

The two, App and Keyboard, talk to each other - the App renders the Keyboard but Keyboard sends back whatever user accepts as their input.

While the top-down communication (from App to Keyboard) is easy, it's just passing down some arguments from the parent component to the child component, the communication from the child to the parent can be implemented in various ways in React, in particular

  • the parent can send a callback down to the child and child can just call the callback
  • both parent and child can share the same Context
  • both parent and child can share a Redux store
  • both parent and child can share a Recoil atom

All these methods are discussed below and implemented in the source code.

Passing a callback (Keyboard1)

This approach involves a callback that is passed from the parent component to the child component. In this case, the callback, onWordTyped is passed between App and Keyboard.

The parent component passes the callback to the child

const App = () => {

    ...

    function onWordTyped( newWord: string ) {
       setWords( ... );   
    }    

    return <>
       ...
       <Keyboard1 ... onWordTyped={onWordTyped} />
    </>;
}

The child component retrieves the callback and uses it to pass the data to the parent

const Keyboard1 = ({..., onWordTyped} : {..., onWordTyped: (word: string) => void}) => {

    ...

    function tryAcceptWord() {
        ...
        onWordTyped(word);
    }
}

This callback-passing style of communication is easy when there's a direct parent-child relation. However, when there are other components between, like App -> Foo -> Bar -> Qux -> Keyboard, passing a callback through the component tree just because there's a component down there below that needs it, would result in more verbose code.

Sharing a Context (Keyboard2)

The shared context approach immediately solves the issue pointed out in above. There's no need for passing down the callback through the component tree anymore.

Instead, all components are wrapped in a context which is a component that exposes some data and actions but the state of the context can be accessed in any component down the component tree, directly.

You start this approach with defining the context

type KeyboardContextPayload = {
    acceptedWord: string
}

type KeyboardContextType = {
    payload: KeyboardContextPayload,
    setPayload: (w: KeyboardContextPayload) => void //React.Dispatch<React.SetStateAction<string>>
};

const KeyboardContext = createContext<KeyboardContextType>({ payload: null, setPayload: null });

export { KeyboardContextPayload };

export default KeyboardContext;

and the context provider component

const KeyboardContextProvider = ({children} : {children: ReactNode}) => {

    const [payload, setPayload] = useState<KeyboardContextPayload>(null);
    const context = {payload, setPayload};

    return <KeyboardContext.Provider value={context}>
        {children}
    </KeyboardContext.Provider>
}

export default KeyboardContextProvider;

and then you just wrap the top-level component in the context so that instead of just

<App />

it's

<KeyboardContextProvider>
    <App />
</KeyboardContextProvider>   

at the top level.

Both the parent and the child component reference the context from the wrapper component

    ...
    const { payload, setPayload } = useContext(KeyboardContext);
    ...

but the child component sets the new state

const Keyboard2 = (...) => {
    ...
    function tryAcceptWord() {
        ...
        setPayload({acceptedWord: word});
    }        
}

and the parent component has an effect hook with payload as the dependency

const App = () => {
    ...
    useEffect( () => {
      ...
    }, [payload] );    
}

A shortcomming of this approach is that it's not that obvious how to define multiple separate contexts (but there are other valid approaches).

Sharing a Redux Store (Keyboard3)

Redux brings a separate data store and a handful of concepts around. Start by adding React-Redux with

npm install @reduxjs/toolkit react-redux

There are actions and reducers but it's easier with thunks (which allow async functions that modify the store) and Immer (which simplifies writing reducers).

To illustrate this concept, the store (/store/store.ts) and a reducer (/store/keyboardSlice.ts) is provided and then Keyboard3 shows how to dispatch an action

const Keyboard3 = () => {
    ...
    function tryAcceptWord() {
        ...
        dispatch(wordTypedActionFactory(word));
    }
}

which is picked by App with

const keyboardInput = useAppSelector(state => state.keyboard);

Note: the Redux store would be a perfect location to store the full list of words typed by user. The reducer's action should just add an element to the list and we could have a separate action to clear the list. In our example, it's the App component that stores the list and the Redux store is only used to pass a single word - the current word typed by the user. This is intentional and just make sure it's always your decision what part of the state is shared in the store.

Sharing a Recoil Atom (Keyboard4)

Recoil is a tiny and simple state management library for React. The atom is defined as

const keyboardStateAtom = atom<KeyboardContextPayload>({
    key: 'keyboardAtom', 
    default: null
});

and it's accessed from the parent and child as

const [keyboardAtom, setKeyboardAtom] = useRecoilState(keyboardStateAtom);

This approach is similar to a shared context, however, it's slightly more flexible as multiple shared atoms are easier to maintain. Also, Recoil introduces the concept of Selectors where it's only a part of an atom (or multiple atoms) is used.

Compilation

To recompile the application, just invoke webpack in the root folder

webpack

Running the application

To run the application, use any HTTP server capable of serving static files. The live-server would do. So, just install the live-server

npm install -g live-server

and then run it from the root folder

live-server

This will host the app under http://127.0.0.1:8080/app.html

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published