How to leverage ReasonML's variant types to create a React wrapper to handle React Native gestures.
In this post, I’m going to go over leveraging ReasonML’s powerful variant types to create a React wrapper to handle React Native gestures. If you’re just getting started with Reason, take a look at my Getting Started with ReasonML and BuckleScript article.
Reason has a powerful variant system that is perfect for handling animations. Variants can model gestures (or other complex actions) that can make reasoning through creating a GestureWrapper far easier.
To get started using React Native and ReasonML, go over the following steps:
1. Install the react-native-cli by running `npm i -g react-native-cli`
2. Create a new react native app by running `react-native init gestureWrapper` and then cd into `./gestureWrapper`
3. Install the Reason dependencies by running `npm i bs-platform reason-react bs-react-native`
4. Add the following scripts to your package.json:
"build": "bsb -make-world -clean-world",
"watch": "bsb -make-world -clean-world -w"
5. Create a `./re` directory and a `./re/app.re` file. Add the following content:
6. Start the app. First run `npm run watch` and then run `react-native run ios`
7. Replace the `index.js` file with the following content:
Side Note: If you are using Windows/Linux, run the application on Android. If you are using yarn, just substitute appropriately. Note that we refer to App in `./lib/js/re/app.js`. JS files are outputed to the lib directory because we have the `in-source` option set to false (or excluded) in our bsconfig.json. `in-source` set to true will generate JS files right next to your Reason files. This certainly makes it easier to refer to assets such as images and refer to the JS file. It’s a matter of preference as to what you prefer.
If everything is done correctly, you should see a white screen with “Hello World” in the top left (it might be somewhat covered up). Let’s go over how this works briefly:
- The index.js file is importing the compiled file and displaying the app on the screen.
Why use Reason? You get a 100% type-safe application that is easier to debug and develop. I’ll show you how this works.
Creating the GestureHandler
Let’s create the basic footprint for the GestureWrapper. We are going to create another file called `./re/gestureHandler.re`. For right now, let’s just create a reducer component that accepts a child. The reducer component needs a state so we’ll skip ahead a little and add some types that we’ll be using later.
Now we’ll go over a couple of things. `open BsReactNative;` will allow us to use the modules in the React Native bindings without having to write `BsReactNative.View`. In this particular snippet, those are the `View` and `Animated` components. You should be careful about how many `open` statements you use in a particular file as it can cause name conflicts.
We use `component` in this file because we will eventually be using some React lifecycle method. We also specifically created a reducer component because we will have some state to deal with later. The reducer component requires a reducer, initialState, and a typed definition of state. Another thing to note here is that we are spreading `children`. That might appear odd if you are just getting started with ReasonReact. The team that developed ReasonReact chose to always wrap `children` in an array (since `children` in practice is usually an array). The key here is how Reason parses the JSX. Let’s say we had the following `<MyLayout> children </MyLayout>`. This actually turns into `ReasonReact.element( MyLayout.make([|children|]) );`. Note how children is now in an array by design. In order to make it not an array, we use the spread operator (i.e. ...`children`).
Side Note: If you’re working through this article, you’ll notice that your editor (and compiler) infer type definitions for you. One of the language’s primary strengths is the ability to infer type definitions, which guarantees 100% type-safe code without the overhead of explicitly defining types. If you need to explicitly annotate a type; take a look at line 28 above. `_state: state` is the annotation used. Although Reason does know the shape of state here, since there is no usage, it can’t be inferred due to a weakly typed definition of state internal to ReasonReact.
Next let’s define our variant by visualizing what our component functionality will look like. Our gestureHandler will actually prevent its child from going off of the screen. Imagine for a second that you are looking at your phone and that there is a draggable image in the middle. The possible gestures that you can take are to:
- Drag the image to one of the 4 corners
- Drag the image to one of the 4 sides
- The image can move and not hit a side or a corner
- You can touch the image without moving it
So let’s define that variant in Reason. For the sake of simplicity we’ll combine the last two gesture types into one variant.
You may ask why `Corner` is accepting a coordinates parameter and why `TopBottom` and `LeftRight` are accepting a `float` parameter. When we move our child to one of these positions, we are going to want to spring it back onto the screen (similar to how Instagram does this functionality when you’re posting). So for a corner we need to know the x, y to move it back onto the screen (similarly for the other variants). The `TouchMovement` variant doesn’t accept any parameters, but we may want to execute a callback.
Next we’ll create out `PanResponder` for our `gestureHandler`. Our `PanResponder` should be part of our component. So we are going to create another instance variable in our state. Note that `initialState` is simply a function that returns the state record. We can use this function to initialize our state variables and then set them.
Side Note: Although we are using a reducer component, we don’t have a need to ever re-render the component based on a state update. So our reducer will remain the same as it is now.
Side Note: `PanResponder.create` accepts a variety of optional arguments. To tell the function we are done, we finish it off with the `unit` type (this is represented as `()`). This is because every function in Reason is curried by default. You can read more here: https://reasonml.github.io/docs/en/function.html
Functions have to match signatures as defined in their respective modules. The type system can really help you by telling you the signature required for the callbacks. Basically a callback expects a function that takes the `nativeEvent` as the first argument and the `gestureState` as the second argument (these have been abbreviated to `_e` and ` _g`).
Side Note: If you’re using VSCode, you can right click on `PanResponder.create` to access the `panResponder.re` file in the node_module. Reading through that will help you understand the types expected. In addition, the compiler will complain and specifically tell you what types are expected if you do not pass in the right types or if your function signatures don’t match what’s expected. It’s good practice in Reason to prefix an underscore on parameters not used in the function body. Without the underscore, the compiler will actually warn you that you have an unused variable.
Before moving on let’s hook up our panHandlers in our render function.
A couple of things to note about the above code-snippet. First we destructure `state` from our `self` variable. Second, notice how styles are formed. I’m not going to spend too much time on styles in this article; however, notice how everything is a function. At the end of the day, Reason is a functional language and you’ll find that most things are in fact functions.
Update your app.re to include an image as the child (just for simplicity).
Notice that we don’t have to import GestureHandler. We just use it based on the file name. Every .re file is a module that you can use throughout your program. At this point you should see a 300x300 image on the top left corner of your simulator/phone screen. Additionally, you should be able to drag it although you’ll notice it’s a little janky at this point.
We’ll primarily focus on the `handleRelease` part of codebase. On release, we’ll check and see where our child is on the screen and then move the child back onto the screen if part of it is off. In order to check the position of our child, we need to know the size of our screen and the size of our child. We’ll create a utility function to get the size of our screen and also let our gestureHandler accept a prop with `childSize`. Additionally we’ll define an additional type called `size`
Note that Reason always returns the last type of a function statement. In this case, the record size. Your editor should also tell you that getWindowSize has a type of unit => size
ReasonReact isn’t going to just throw a warning if you don’t pass in a `childSize`, it’s actually not going to compile. Required props are actually required for compilation. So let’s fix our app.re.
We need a couple more functions in order to make this work. I’m going to create these quickly and then run through them 1 by 1.
This may seem like a lot at once, but let’s break it down.
- `resetPan` calls extract offset. Per the React Native docs: Sets the offset value to the base value, and resets the base value to zero. The final output of the value is unchanged. Essentially this will allow us to move the child to point A and to point B from point A.
- `moveChild` handles the automated move of the child (if it’s off the screen). It has an optional float parameter of duration. What is |. though? The fast pipe operator takes what’s on the left and sticks it into the first argument for the function on the right. |>, another pipe operator, does the same thing except that it puts what’s on the left as the last argument for the function on the right. It’s a nicer way to write function composition rather than being worried about a million parentheses and not knowing what’s going on.
- `handleGestureType` handles the gestureType variant defined above.
- `handleRelease` is the bulk of this component and really shows off the power of Reason’s pattern matching. We defined the corners of the child first and then grab the current window size. We put the four corners into a tuple and then use a switch statement to run through all the possible variations for where the child is on the screen. The switch statement returns a curried function handleGestureType and pan is piped in to execute the action. In a nutshell, Reason really shows off its elegance here. The language is incredibly powerful at handling complex logic and making it look simple.
Lastly we have to hook up `childCoordinates`. We’ll need to listen to the value of `pan` and set `childCoordinates` to the value. In addition, we’ll want to make sure we remove the listener when the component unmounts. We’ll need another state variable to hold the id of our listener.
There are only a couple of changes from the previous component. `willUnmount` will remove the listener. In our `SetPanResponder` listener, we set up the listener. In addition we pass the proper arguments into the `handleRelease` function. So here are the final results:
. . .
My source code for the above can be found here. I hope you enjoy this language as much as I’ve been enjoying it. I always love hearing feedback about my work. Let me know if you have any questions, and I’ll be happy to help out. Thanks for reading.
A special thanks to G2i for this post. For those whom are unfamiliar, G2i is a hiring platform run by engineers that matches companies with pre-vetted React, React Native, GraphQL, and native iOS/Android focused engineers you can trust. Check them out!