Learn ReasonML by Building Tic Tac Toe in React

ReasonML is a pragmatic new syntax for the battle-tested OCaml language that offers strong typing, powerful pattern matching, and functional programming concepts. Developed by Facebook, ReasonML integrates seamlessly with React via the ReasonReact library, enabling you to build robust and maintainable web applications.

In this tutorial, we‘ll dive into ReasonML by building the classic game of Tic Tac Toe in React. Step by step, you‘ll learn core ReasonML concepts as we implement the game logic and UI. By the end, you‘ll have a working game along with an understanding of how to leverage ReasonML‘s features in your own projects. Let‘s get started!

Setting Up ReasonML

First, make sure you have Node.js and npm installed. Then use npm to install the BuckleScript compiler and ReasonReact:

npm install -g bs-platform 
npm install --save reason-react

BuckleScript compiles ReasonML to readable JavaScript code. With the compiler installed, create a new directory for the project and generate a ReasonReact app:

bsb -init tic-tac-toe -theme react-hooks

This sets up a basic project structure. The ReasonML source files have an .re extension and are located in the src directory.

Defining Types

One benefit of ReasonML is its strong static type system. Let‘s define some types to represent the game state. Open the TicTacToe.re file and add:

type player = 
  | X
  | O;

type field = 
  | Empty 
  | Marked(player);

type row = list(field);
type board = list(row);

type gameState = 
  | Playing(player)  
  | Winner(player)
  | Draw;

type state = {
  board: board,
  gameState: gameState,
};  

Here we define variant types for a player (X or O), a board field (Empty or Marked by a player), a row (list of fields), the full board (list of rows), and the game state (Playing, Winner, or Draw). Finally, the overall game state is defined as a record with a board and gameState.

ReasonML will infer types in many cases, but explicitly defining them documents the code and catches errors. Variant types are a powerful way to model data that can be one of several possible cases.

Initializing State

Next, let‘s set the initial state for a new game:

let initialState = {
  board: [
    [Empty, Empty, Empty],
    [Empty, Empty, Empty],
    [Empty, Empty, Empty],
  ],
  gameState: Playing(X),
};

We create a 3×3 board (list of rows, each a list of Empty fields) and specify the game state is Playing with X to move first.

Rendering the Board

Now we can start defining the React components to render the game. First let‘s create a Square component to display each board field:

[@react.component]
let make = (~field: field, ~onMove) => {
  <button onClick=onMove>
    {switch (field) {
      | Empty => React.string("")
      | Marked(X) => React.string("X") 
      | Marked(O) => React.string("O")
    }} 
  </button>
};

This is a stateless component that takes a field prop and renders a button displaying X, O, or nothing based on the field value. It also takes an onMove prop to handle click events.

The [@react.component] decorator tells ReasonReact this is a component function. Parameters are specified with the ~ syntax.

Next, let‘s make a Board component that renders the 3×3 grid of squares:

[@react.component]
let make = (~board: board, ~onMove) => {
  <div>
    {board
      ->Belt.List.mapWithIndex((y, row) => {
          <div key={"row" ++ string_of_int(y)}>
            {row
              ->Belt.List.mapWithIndex((x, field) => {
                  <Square 
                    key={"col" ++ string_of_int(x)}
                    field
                    onMove={() => onMove(x, y)} 
                  />
                })
              ->React.list}
          </div>
        })
      ->React.list}
  </div>
};  

Here we use the pipe operator -> to essentially "pipe" the board prop through a series of transformations. Belt.List is ReasonML‘s standard library for list operations. We map over the board rows and fields to create a nested structure of divs and Square components.

List.mapWithIndex gives us an index value to use for React keys. ReasonML has built-in functions like string_of_int for common type conversions. The x and y index values are passed to the onMove handler.

Finally, React.list transforms the list of elements into an actual React array that can be rendered.

Handling Moves

Let‘s implement the game logic now, starting with what happens when a player clicks a square. In the parent Game component, we define a moveReducer function for handling the state update:

type action = 
  | Move(int, int);

let moveReducer = (state, action) => {
  switch (action) {
  | Move(x, y) =>
    let newBoard = state.board
      ->Belt.List.mapWithIndex((yi, row) =>
          row->Belt.List.mapWithIndex((xi, field) =>
              if (xi == x && yi == y) {
                switch (state.gameState, field) {
                | (Playing(player), Empty) => Marked(player)
                | _ => field
                }
              } else {
                field
              }
            )  
        );
    {...state, board: newBoard, gameState: calculateGameState(newBoard)};
  };
};

When a Move action is dispatched with the square‘s x and y coordinates, we use them to map over the board until we find the matching position.

If the square is Empty and the game is in Playing state, we mark it with the current player. Otherwise the field is left unchanged.

We use the spread operator … to create a new state record with the updated board. The new game state is calculated by a helper function we‘ll define next.

Determining the Winner

The calculateGameState function checks if the game is won or a draw after each move:

let calculateWinner = (board, player) => {
  let winningMoves = [
    // Rows
    [(0, 0), (0, 1), (0, 2)],
    [(1, 0), (1, 1), (1, 2)], 
    [(2, 0), (2, 1), (2, 2)],
    // Columns 
    [(0, 0), (1, 0), (2, 0)],
    [(0, 1), (1, 1), (2, 1)],
    [(0, 2), (1, 2), (2, 2)],
    // Diagonals
    [(0, 0), (1, 1), (2, 2)],
    [(0, 2), (1, 1), (2, 0)], 
  ];

  winningMoves->Belt.List.some(move => {
      move
        ->Belt.List.map(((x, y)) => board[y][x])
        ->Belt.List.every(field => field == Marked(player))
    });
};

let calculateGameState = board => {
  if (calculateWinner(board, X)) {
    Winner(X);
  } else if (calculateWinner(board, O)) {  
    Winner(O);
  } else if (board->Belt.List.every(Belt.List.every(field => field != Empty))) {
    Draw;
  } else {
    switch (gameState) {
    | Playing(X) => Playing(O)
    | Playing(O) => Playing(X) 
    };  
  };  
};

The winning moves are hardcoded as a list of coordinate triples. We check if any of the moves have been completely filled by a single player using List.every.

If there are no winners but the board is full (checked with a nested every call), the game is a draw. Otherwise, we switch the current player.

Resetting the Game

Finally, let‘s add a button to reset the game. In the Game component:

type state = {
  board: board,
  gameState: gameState,
};

type action =
  | Move(int, int)
  | Reset;

[@react.component]
let make = () => {
  let (state, dispatch) = React.useReducer(reducer, initialState);

  <div>
    <Board board={state.board} onMove={(x, y) => dispatch(Move(x, y))} />
    {switch (state.gameState) {
      | Winner(player) => React.string("Winner: " ++ string_of_player(player))  
      | Draw => React.string("It‘s a draw!")
      | _ => React.null 
    }}
    <button onClick={_e => dispatch(Reset)}>
      {React.string("Reset game")}
    </button> 
  </div>;
};

We add a Reset action type and extend the reducer to handle it by returning the initialState. The Reset button dispatches this action onClick.

We also display a message when the game is won or ends in a draw.

Conclusion

Congratulations, you now have a fully functioning Tic Tac Toe game in ReasonML and React! The full code is available here.

Some key takeaways from this tutorial:

  • ReasonML‘s type system helps catch errors and documents your code
  • Variant types are powerful for modeling state and data
  • The pipe operator enables clean functional transformations
  • Pattern matching allows you to handle different action and state cases
  • ReasonML compiles to readable JavaScript and integrates smoothly with React

I encourage you to extend this game further – some ideas:

  • Add styling with CSS
  • Display move history
  • Highlight the winning sequence
  • Expand to a N x N board size
  • Add an AI opponent

This is just a small taste of ReasonML‘s capabilities. To go deeper, check out the official ReasonML docs and ReasonReact docs.

I hope this tutorial piqued your interest in ReasonML. The language offers a compelling mix of type safety, expressiveness, and familiarity for JavaScript developers. Give it a try in your next project!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *