React 101, Part 1: Building a GIF search engine

Feature image: React 101, Part 1: Building a GIF search engine
This post is 1st in the React 101 series.

Articles in the React 101 Series:

Last updated 06/01/17 to use Create React App, Redux Form v6, React-Router v4, and Firebase v3. Looking for the old version of the code? Find it here

Table of Contents:

Quick note: I created a repo on GitHub to show the steps of building this app, so I'll be linking to commits in that repo along the way. You can view the seven steps of the process in the GitHub commit history, but I'll also link each step alongside the article.

Why React?

A lot of people are excited about React, Facebook's new JavaScript framework. Big companies, among them Netflix, Yahoo!, Atlassian, and Khan Academy, are making React a critical part of their stack, and it seems like more and more developers are singing its praises (or, alternatively, complaining about JavaScript fatigue).

React is different. Really different. Almost every single previous popular library has focused on connecting JavaScript to your HTML inside of your HTML — such as jQuery's referencing elements in your HTML by their id selectors or Angular's ng- syntax.

React has flipped this paradigm on its head. Instead of writing HTML files and then figuring out how to pull in your JavaScript, you instead write JavaScript files and render your HTML from inside.

Most JavaScript frameworks operate by manipulating the DOM directly, but this can be very slow. React creates a copy of the DOM — known as the virtual DOM — and manipulates that instead. It then compares the virtual DOM with the real DOM and swaps out any changed pieces in a more efficient manner than is possible when trying to update the real DOM directly.

React isn't a MVC framework like Angular or Ember — in fact, it is only the V, or the view layer, in MVC. It doesn't concern itself with modeling your data or routing, and there are no controllers to deal with. Instead, you use React to build interactive and reusable UI components.

React itself is reasonably well-documented and easy to pick up, once you can shift your thinking to align with its conventions. The problem is, if you want to build robust apps, the V in MVC probably isn't going to cut it, and you have to dive into the often-confusing ecosystem surrounding React.

In this series, we're going to walk through the stages of building a React application — an app that lets you search the Giphy API and displays results, similar to what Giphy has on its own website.

  • In this first article, we are going to build the application using only React (with Webpack for asset compilation).
  • In the second article, we're going to add in Redux, a popular library that helps handle state in a more predictable way.
  • In the third article, we're going to add routing and some forms, and we're also going to scaffold out an authentication system
  • In the final article, we're going to connect to Firebase, a popular NoSQL cloud database

You can find the Github repository with step-by-step commits here.

IMPORTANT NOTE: While we try to keep this tutorial updated for the latest versions of all the packages, things can change fast in the JavaScript ecosystem. If you are getting unexpected errors, your best bet is to check the package.json file in our project repository and make sure you're using the same versions that we used in the tutorial.

Prerequisites

You don't need to have any React experience to work through this series, but you should probably have at least an intermediate-level background in JavaScript to understand what's going on.

You'll also want to have Node.js/npm installed. I will be using version 6.5.0 for this project.

If you've never used Webpack before, you might also want to take a look at our Unpacking Webpack article before diving in. While we're going to use a tool that handles most of the configuration for us, you might find it helpful to understand what's going on under the hood.

Ready? Let's begin!

Setting up your first React Project

Step 1 commit on GitHub

In the previous version of this tutorial from March 2016, we had several sections dedicated to setting up React, Webpack, and Babel (so that we could write our code in ES6). However, a few months after we published the series, Facebook released Create React App, a command line tool to build React apps with zero build configuration.

To create our project, simply head over the console and enter the following commands:

$ npm install -g create-react-app
$ create-react-app react-gif-search

Out of the box, we now have access to the following tools:

  • React: The JavaScript framework we'll be using to build our app
  • Webpack: An asset bundler that takes all of our JS/CSS files, combines them into one file, and serves that file. It also includes Hot Reloading, which means that we should see any code changes without having to refresh our browser!
  • Babel: A compilation tool that allows us to write ES2015 (also commonly referred to as ES6) JavaScript code. We will spend more time talking about ES2015 features as we come across them.
  • ESLint: A JavaScript linting utility that checks our JavaScript code for spacing issues, errors, etc.

If you run $ npm start in your console, your browser should open automatically and navigate to http://localhost:3000:

Create React App default screen

Before we get started, we're going to do a bit of cleanup on some of the boilerplate files generated by Create React App. Within the src/ directory, delete every file except index.js. For that file, you can delete all the code so we have an entirely fresh start.

The fundamentals of React

To understand React, you need to think in terms of self-contained components. A component is a module that contains all of the HTML and JS it needs (and, often, the CSS as well). It also only handles its state or the state of its direct children, making it far easier to track the flow of data.

What do I mean by "state?" State can be any sort of data: for example, whether a checkbox is toggled, whether a user is logged in, what gif is currently selected.

The app we are going to create takes text input from a search field and returns a list of individual gifs. This can be broken down into four separate React components, as seen here:

app components

  • The App component is a bucket for the rest of our modules — it will manage our base state and coordinate between the child components underneath it. For this simple version of our application, it will also handle making API calls to request GIFs, although we will quickly see the limitations of this approach and refactor it in part 2 by adding Redux
  • The SearchBar component will watch for a change to the user input and pass the search term(s) back up to the App component when needed
  • The GifList will map the array of gifs returned from the Giphy API and handle rendering individual GifItems
  • GifItems will display the gifs

To see how we can render data with our first component, let's start building our App.

First, go to the index.html file in the root of your app and replace its code with the following:

index.html

Now we get to one of the coolest things about React: this is the only HTML file we need. Remember how we mentioned earlier that React involves writing HTML inside of JavaScript? React will hook into the <div> element we created and handle rendering our entire application inside of there. Neat, right?

Let's build our first simple React component:

src/index.js

If you run $ npm start and load up http://localhost:3000 in your browser, you should see our 'Hello World' message on your screen.

There's a lot going on in this code, especially if you're not used to ES2015, so let's break it down piece by piece:

import is the ES6 version of require. We are pulling in two libraries here: the base React library, which gives us the code we need to create and manage components, and React DOM, which helps us manipulate elements within the browser. Until recently, they were part of the same package, but the React team split them in two to make it easier to render React on different environments (such as on mobile with React Native.)

Here, we're creating an App component that will serve as the parent for the rest of our application.

The class constructor is another new feature of ES2015. While this looks like a completely new object-oriented way to write code in JavaScript, similar to what we might find in a language such as PHP or Ruby, it's actually little more than syntactical sugar for creating a plain old JavaScript object. If we were writing ES5, we would create a React class like this:

var App = React.createClass({});

The class constructor may feel natural to you if you've used another object-oriented programming language, or it may feel weird if you're used to traditional prototypal JavaScript. I encourage you to give it a chance, though; it can help a lot with organizing code in a large-scale project, especially if you're using an IDE like Webstorm.

This is where the magic happens. React's render function allows us to output JSX, which is syntax falling about halfway between XML and HTML that creates JavaScript objects for us.

The code above in JSX is equivalent to this code in native JavaScript:

return React.createElement(
"div",
{ "class": "greeting" },
React.createElement(
"p",
{ "class": "greeting-text" },
"Hello World!"
)
);

Babel, one of the libraries included with the Create React App tool, handles this transformation for us. As you can see, JSX is much easier to write!

And finally:

Remember how I mentioned earlier that React-DOM handles any browser-specific manipulation that React needs to do? In the ReactDOM.render() method — not to be confused with the render() method in React.Component — we link our App component with the empty <div id="app"></div> in our index.html file.

Adding a second component, and listening to user input

Step 2 commit on GitHub

Let's go ahead and create our Search Bar component within a new src/components folder. For now, let's just output a <h1> so we can be sure we're pulling it into our main App component correctly.

src/components/SearchBar.js

Most of this looks very similar to the App component we already created. The major difference is the last line, export default SearchBar, which is another new ES2015 feature.

ES2015's export works similarly to the module.exports functionality of ES5. The export makes our SearchBar available to import by other pieces of our application, as we are about to do in our App class.

The default means that this module is only exporting one value — in this case, the class SearchBar.

Next, let's update our index.js file to bring in our first child component.

src/index.js

We are doing two important things here:

  • Importing the SearchBar class from ./components/SearchBar at the top of our file
  • Adding it to our render method as a self-closing component. (Please note that <SearchBar> alone is not valid JSX; if the component does not have a body, it is important to add the /> to close the tag.)

We now should see this in our browser:

search output

Let's refactor this code to be more interactive. We need to create an input field in our SearchBar component and be able to track what term we are searching for in our main App component so that we can use it to search the Giphy API.

Before we do this, though, we need to let our React components know they should keep track of our term. We can do this by setting it as part of the state of our application. State is the data that will change over time; we want to initialize our state with a default value and then watch for any updates. Every single time React notices a change in state, it will re-render with the value of the new state.

Let's look at how we might do that with our search bar, and then we'll walk through the code piece by piece:

src/index.js

src/components/SearchBar.js

Let's start by taking a closer look at the updated SearchBar component.

We added a constructor() method at the top of the class. Similar to what you might find in other OOP languages, the constuctor runs automatically when the class is initialized. Calling super() within that method lets us access this.state within the constructor, since our class is a subclass of React.Component.

We are already extending React.Component when initializing our SearchBar class, but if we want access to React's this.state in the constuctor, we need to make sure that our class is inheriting whatever properties are inside of the constructor of the parent ReactComponent.

We are also initializing our state here, letting our application know that it needs to be aware of our search term. For now, we're setting it to an empty string, but we'll want to be able to update it through our input field.

For now, let's hold off on talking about our onInputChange() method and take a look at what's happening in our render().

Why are we using className instead of class here? Remember that we're writing JSX, not HTML. class is a reserved keyword in JavaScript, so we must use className instead to set HTML classes. (The same thing is true about for: if your HTML element requires for, you should use htmlFor instead.)

Every time we update our input, React's onChange property automatically fires.

You probably noticed the odd-looking syntax within the curly braces. This new ES2015 feature is known as fat-arrow functions; they're a stripped-down way of writing functions, similar to what you might find in CoffeeScript.

A fat-arrow function like this:

e => this.onInputChange(e.target.value)

is equivalent to the following code:

function(event) {
this.onInputChange(event.target.value);
}

Here, we're telling React that every time it notices we've changed our input, it should fire an onChange event and pass the value — our search term — to our onInputChange() class method. Let's take a closer look at that now:

We now know that our onInputChange() method will be fired every time the input is changed. You might be wondering, however, why we are calling this.setState instead of setting the term directly on this.state, as we did in the constructor.

This is one of the most important things to remember when you're working with React. When you're initializing state in the constructor, you can set it directly with this.state = {}. However, if you want to signal to React that the state has changed so that it knows to re-render, you need to call this.setState() instead. This means you should never call this.state = {} outside of a class constructor.

But where does our this.props.onTermChange(term); come from? We need a way to pass data from a child component (SearchBar) to its parent (App), and in React, we can do that through props -- data or callbacks passed from a parent component. To see how we can do that, let's take a look at the code we added to our index.js file:

In our <SearchBar /> within index.js, we are setting a new property called onTermChange. Whenever we set a property on a child component in this way, it becomes available within that child component via this.props. In this example, we are using the onTermChange property to pass the handleTermChange() callback from our App our SearchBar.

Confused? Let's break down what is actually happening, step-by-step, from the time we open the app in our browser:

  • Our App component renders our SearchBar component with a prop of onTermChange, passing in its own handleTermChange method as an argument
  • Our SearchBar calls its constructor() method upon initialization. It creates a new state object and sets the term propery to an empty string.
  • The user enters some text in the input field
  • Every time the user enters/deletes a character, React calls the onChange method on the input, automatically passing in an event object as an argument. Within its callback, we call our SearchBar class's onInputChange class method, passing through the event object.
  • The SearchBar's onInputChange method calls this.setState to update the state's term property. It also calls the App component's handleTermChange method, which is passed through the onTermChange prop.
  • Our App logs the search term to the console via the handleTermChange method

To test that this is working, open your developer console within the browser and enter some text into the search bar. You should see the console log the new term every time you add or remove a letter.

console output

State, nesting components, and stateless functional components

Step 3 commit on GitHub

It's time to start building the components that will handle displaying gifs! Let's take another look at our app mockup:

app components

There are two different components we will need to build here: a GifList that maps through the array of gifs we will receive from the Giphy API and a GifItem to actually render the individual gifs.

Before we actually worry about calling the Giphy API, let's mock up some fake data to pass to our components. In index.js, add a constructor function and initialize the state with an array of gifs. Don't forget to call super() so that we actually have access to this.state! We're also going to create a GifList component and place it directly below our SearchBar, passing through the gifs object we set on our App's state as a prop.

src/index.js

Next, we need to create our GifList component. Its sole job is to accept a list of gifs from the App component, loop through them, and render an individual GifItem for each object in the array. Let's take a look at the code:

src/components/GifList.js

You might have noticed that this component looks different than the SearchBar and App. Instead of using class GifList extends React.Component like we did before, we're instead writing const GifList = (props) => {}.

This is called a stateless functional component. We can use these whenever our component does not need to actively track or modify our application's state — in fact, if you're writing idiomatic React code, most of your components will be written this way. The parent component — in this case, App — passes in all of the data our GifList needs via its props. The GifList only needs to worry about how to display this data.

Once again, we are seeing the new ES2015 syntax for writing functions. We are also seeing the new ES2015 keyword const, short for "constant", which allows you to declare variables that won't be reassigned.

In short, this:

const GifList = (props) => {}

is the equivalent of this:

var GifList = function(props) {}

Within this function, we are looping through the array of gifs passed down from state. For each gif, we are rendering a GifItem component.

Notice that we are setting a key property on each GifItem. In React, if you have multiple components of the same type, you should give each a unique key.

Why do we care about this? When React manipulates the virtual DOM, it tries to update as few modules as it can. If it can't tell GifItem components apart, it will need to re-render all of them whenever an update is made.

Our GifItem component is even simpler:

src/components/GifItem.js

Once again, we are creating a stateless functional component here. Our GifItem takes the image object passed as props from the GifList and passes the URL into an image element.

Now, if we check http://localhost:3000, we should see our three images rendering on the page beneath our search bar:

static gif list

Obviously, our search engine isn't going to be very exciting with hard-coded data! Let's take a look at how we can call the Giphy API.

AJAX and APIs

Step 4 commit on GitHub

While there are a lot of ways to make HTTP requests within the Node.js ecosystem, a library called SuperAgent is one of the best-documented and easiest to use. Run $ npm install --save superagent to pull it into your project.

Back in our index.js file, clear out the hard-coded list of gifs in our constructor. We do still want to initialize a gifs object within this.state so our application is aware that it needs to track changes, but we can set it to an empty array.

src/index.js

If you take a look at the Giphy API Docs, they walk you through making an API call to an endpoint. We will need three things in order to accomplish this:

  • The base API url, which is https://api.giphy.com/v1/gifs/search?q=
  • The term(s) we want to search for
  • An API key. To get one, we'll need to sign up with Giphy and create an app, at which point we'll be assigned a test API key we can use during development.

Since we are already receiving a search term from our input as an argument in our handleTermChange method within index.js, we should have everything we need to make API calls.

Let's take a look at how we might do that. For now, let's console.log the first gif that comes back so we know what sort of data we're working with:

src/index.js

After importing SuperAgent's request object, we want to make our API call within our handleTermChange method, since that's receiving the term from our SearchBar.

We're setting a const named url with all three things we said we needed to make an API call. Notice the ${term} in the middle of that string; this is yet another bit of ES2015 sugar. Adding ${} to a string allows us to concatenate a variable, but in order to use this syntax, you must wrap the string in backticks `` instead of single or double quotation marks.

Finally, we're making a GET request using SuperAgent to the URL we specified. You can find out more about how to make requests with SuperAgent in their documentation, but for now, we're going to keep it as simple as possible.

If you search for test in the search bar, you should see in your console something like this:

superagent giphy response

So we now know that we will pass an array of objects like this into our GifList, and our GifItem will need to use these attributes to render a gif.

Let's refactor our code to pass the Giphy data into our components via this.setState. Additionally, we are going to make one more update. If we are searching for multiple terms, they will probably be separated by a space; however, if we are passing them to the Giphy API, they will need to be separated by a + character. Instead of passing just term to our url const, let's write a regex to replace all spaces with +'s, like so: term.replace(/\s/g, '+').

src/index.js

But wait! If we try to search again, we will see the following error in our console:

bundle.js:8088 Uncaught TypeError: _this2.setState is not a function

Why is this? When we are using ES2015, React does not automatically assume that event handlers we pass as properties (such as onTermChange in our SearchBar) are bound to the main component object. (While this bit of automagical binding can be useful 99% of the time, if you're passing in event handlers from outside of your main React application, it can be incredibly limiting. They decided to follow the same semantics as regular ES2015 classes, binding this to its own instance rather than autobinding to the parent component. This is also why we need to call super() in our constructors!)

TL;DR: this means that this.setState is actually being called on onTermChange, which does not have access to the React.Component methods inherited by our App component.

This is one of the more confusing gotchas involved in working with React, but there are two ways around it. Since you'll see both of them out in the wild, let's look at them both:

**Binding in the Constructor**

The first workaround is to explicitly tell our app that the lexical this of the handleTermChange method is bound to App, not onTermChange. We can do this with the following code in our constructor:

src/index.js

**Fat-Arrow Functions**

We've seen ES6's fat-arrow functions syntax a few times already, but one of the great things about it is that it doesn't introduce its own this. Because it inherits the lexical this of its parent scope, we don't run into issues with binding. (This is also why we didn't see this issue in our SearchBar component.)

If you're going to use fat arrow functions, you can write them two different ways:

  • As a class method: src/index.js

  • As part of the handler:

This can be confusing, especially if you haven't run into a lot of scoping issues with this in JavaScript before, but if you see this error you should hopefully know how to fix it!

Moving on, we'll need to update our GifItem component to handle the data from Giphy instead of our hard-coded gif objects. If you take a look again at the objects being returned, you can see that the main gif object has an images property that contains quite a few different size options. Because we don't want any massive images loading, let's use the downsized object.

src/components/GifItem.js

Now, if we search for test, we should see something like this:

unstyled gifs

It's ugly, but it works! Let's add a bit of styling next.

Making it all look good—CSS and Webpack

Step 5 commit on GitHub

If you read Unpacking Webpack, you should already be somewhat familiar with handling CSS in Webpack. We are able to import our CSS directly into our JavaScript, and best of all, Create React App handles all of the configuration for us!

Included below is the styling we'll be using. If you're at all familiar with CSS, there shouldn't be anything too confusing here:

src/styles/app.css

We'll also want to import app.css into our index.js file so that Webpack knows to bundle our CSS with our JavaScript:

src/index.js

Finally, we'll want to add CSS classes to our GifList and GifItem components. Remember that we need to use className, not class, since our render method contains JSX and not HTML! We are also going to use divs instead of uls and lis, since we're using more of a Masonry-style layout.

src/components/GifList.js

src/components/GifItem.js

Now, if we search for test again, we should see a (slightly more) nicely-styled list:

styled gifs

There's one more step in part one of this tutorial! Since we're getting all of this metadata back from Giphy, let's add a modal pop-up whenever the user clicks on the gif.

Adding a modal with a third-party package

Step 6 commit on GitHub

One of the great things about the modularity of React components is how easy it is to plug-and-play third-party packages. While we could go through the trouble of writing our own modal from scratch, we're instead going to pull in react-modal and let it handle most of the heavy lifting for us.

To pull the package into our project, run $ npm install --save react-modal.

Currently, we have two pieces of state we're tracking in our application. Our App component is responsible for handling an array of gifs, and our SearchBar component tracks the search term entered in the input.

For our modal, there are two additional pieces of state we need to track:

  • Is the modal currently open?
  • What is the currently selected gif we should display in the modal?

We also know that we are going to need to handle two interactions: when the user clicks on a GifItem component to open the modal and when they click the "close" button to close the modal.

In our index.js file, let's add the new code we'll need to be able to track our state:

src/index.js

As we did with the gifs previously, we are creating empty objects in this.state within our constructor so that our app can track the relevant data. We're also trying to approximately match the type of objects within our state — we know gifs is an array, selectedGif is a single object, and modalIsOpen is a boolean, so we can initialize them accordingly.

We are then creating two methods to open and close our modal for us. The openModal() method will accept our selected gif image as an argument, and closeModal sets it to null once again.

If you take a look at the react-modal docs, you can see that they provide us with a sample modal component to get started:

<Modal
isOpen={bool}
onRequestClose={fn}
closeTimeoutMS={n}
style={customStyle}>
 
<h1>Modal Content</h1>
<p>Etc.</p>
</Modal>

We could put this directly into our App component, but in the spirit of modularity, let's create a separate GifModal component as a wrapper for the Modal component provided by react-modal.

Because this component won't need to track state — our App will be handling whether or not the modal should be open and passing this data in as props — we can make this a functional component.

src/components/GifModal.js

Working directly off of the sample Modal component in the react-modal docs, we can see that we need at least two properties: isOpen and onRequestClose. Because onRequestClose takes a function, we'll need to pass a handler down from our App component and tie it to the closeModal method we already created.

We're also adding a bit of basic JSX here to display the full-sized gif (not the downsized one we're using in GifItem) along with the source and rating coming back from the Giphy API.

Going back to our index.js file, let's plug in our GifModal component:

src/index.js

Here, we're passing in our our modalIsOpen and selectedGif objects from state (even though we don't yet have a way to open a modal or select a gif.) We're also passing in our closeModal() method via a prop; whenever our third-party Modal component calls onRequestClose, such as when clicking outside of the modal or hitting the "close" button, it will call our App component's closeModal() function.

But when we take a look in the browser, we see the following error:

Uncaught TypeError: Cannot read property 'images' of null

Digging into our bundle.js file, we can see that it's looking for props.selectedGif.images and throwing an error because the images property does not exist on the current null selectedGif. We know we occasionally want selectedGif to be null, though, so let's add a check at the beginning of our component to return an empty div if this property doesn't exist:

src/components/GifModal.js

That should fix the error!

Next, we need our App component to know when a gif is clicked so that it can set selectedGif and modalIsOpen on state accordingly. Once again, we're going to have to pass a handler down into a child component as a prop, but this time, we'll have to do it twice, first to GifList and then to GifItem.

Let's start from the initial click event. On our GifItem component, we will need an onClick handler to pass the component's gif prop to a handler sent all the way down from App. Let's call that prop onGifSelect.

We're going to make one more update as well. Currently, our GifItem is only expecting a prop object called image. Because image.onGifSelect() doesn't make as much sense, we're going to do a bit of ES2015 shorthand to assign any objects underneath image to their own vars.

So instead of:

const GifItem = (image) => {

which gives us image.gif and image.onGifSelect, we can write:

const GifItem = ({gif, onGifSelect}) => {

If we do that, we can access our props as gif and onGifSelect directly!

Moving on, we know we need to pass onGifSelect as a prop from GifList to GifItem:

src/components/GifItem.js

src/components/GifList.js

We don't need a lot of code to accomplish this — GifList will expect an onGifSelect prop passed down from the App component, but ultimately, it's not concerned about what else is being passed back and forth.

Finally, back in index.js, we need to add the onGifSelect handler and pass in our openModal method:

src/index.js

There's a lot to process here. Let's slow down and go through the steps from the time the user clicks on a gif to open a modal:

  • The user clicks a gif
  • The GifItem component receives an onClick event anywhere in its main <div>
  • It passes its gif prop as an argument into its onGifSelect prop
  • The GifList receives the selectedGif. It, too, has an onGifSelect prop, and through this handler, it passes the selectedGif to its parent component
  • The parent App component receives the selectedGif and passes it into its openModal method
  • The openModal method sets the selectedGif on state and also sets modalIsOpen to true
  • Because both modalIsOpen and selectedGif are props on the GifModal component, when React calls this.setState (not this.state = {}!), it knows it needs to re-render GifModal
  • GifModal receives modalIsOpen and selectedGif as props, along with an onRequestClose() handler
  • GifModal passes modalIsOpen and onRequestClose() as props to the third-party Modal component and renders the selectedGif
  • The user clicks the close button
  • The GifModal's onClick handler fires, calling the closeModal() function passed down from App
  • The modal closes and selectedGif is set to null
  • Our GifModal is re-rendered due to the new state and, because it has received an empty object for props.selectedGif, returns an empty div

We have two more small updates to make! First, let's adjust the styling on our GifModal so that it's not quite as ugly:

src/components/GifModal.js

src/styles/app.css

Finally, let's add some placeholder text on our SearchBar to give the user some idea of what to do:

src/components/SearchBar.js

Now, when you search for a gif and open the modal, you should see something like this:

gif modal styled

Conclusion

That wraps it up for part one!

While vanilla React projects can be fine if you're working at a relatively small scale, I'm sure you can see even from this little project how easy it would be for state to get out of control when it's sprinkled all over the place. In part two, we're going to refactor this project to use Redux, a library that allows you to store the entire state of your application within a single object tree.

Check back soon, and as always, we'd love to hear any thoughts/comments on Twitter.

Get our latest insights in your inbox:

By submitting this form, you acknowledge our Privacy Notice.

Hey, let’s talk.
©2024 Tighten Co.
· Privacy Policy