One place for hosting & domains

      Components

      How To Manage State with Hooks on React Components


      The author selected Creative Commons to receive a donation as part of the Write for DOnations program.

      Introduction

      In React development, keeping track of how your application data changes over time is called state management. By managing the state of your application, you will be able to make dynamic apps that respond to user input. There are many methods of managing state in React, including class-based state management and third-party libraries like Redux. In this tutorial, you’ll manage state on functional components using a method encouraged by the official React documentation: Hooks.

      Hooks are a broad set of tools that run custom functions when a component’s props change. Since this method of state management doesn’t require you to use classes, developers can use Hooks to write shorter, more readable code that is easy to share and maintain. One of the main differences between Hooks and class-based state management is that there is no single object that holds all of the state. Instead, you can break up state into multiple pieces that you can update independently.

      Throughout this tutorial, you’ll learn how to set state using the useState and useReducer Hooks. The useState Hook is valuable when setting a value without referencing the current state; the useReducer Hook is useful when you need to reference a previous value or when you have different actions the require complex data manipulations. To explore these different ways of setting state, you’ll create a product page component with a shopping cart that you’ll update by adding purchases from a list of options. By the end of this tutorial, you’ll be comfortable managing state in a functional component using Hooks, and you’ll have a foundation for more advanced Hooks such as useEffect, useMemo, and useContext.

      Prerequisites

      Step 1 – Setting Initial State in a Component

      In this step, you’ll set the initial state on a component by assigning the initial state to a custom variable using the useState Hook. To explore Hooks, you’ll make a product page with a shopping cart, then display the initial values based on the state. By the end of the step, you’ll know the different ways to hold a state value using Hooks and when to use state rather than a prop or a static value.

      Start by creating a directory for a Product component:

      • mkdir src/components/Product

      Next, open up a file called Product.js in the Product directory:

      • nano src/components/Product/Product.js

      Start by creating a component with no state. The component will consist of two parts: the cart, which has the number of items and the total price, and the product, which has a button to add or remove the item from the cart. For now, these buttons will have no function.

      Add the following code to the file:

      hooks-tutorial/src/components/Product/Product.js

      import React from 'react';
      import './Product.css';
      
      export default function Product() {
        return(
          <div className="wrapper">
            <div>
              Shopping Cart: 0 total items.
            </div>
            <div>Total: 0</div>
      
            <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
            <button>Add</button> <button>Remove</button>
          </div>
        )
      }
      

      In this code, you used JSX to create the HTML elements for the Product component, with an ice cream emoji to represent the product. In addition, two of the <div> elements have class names so you can add some basic CSS styling.

      Save and close the file, then create a new file called Product.css in the Product directory:

      • nano src/components/Product/Product.css

      Add some styling to increase the font size for the text and the emoji:

      hooks-tutorial/src/components/Product/Product.css

      .product span {
          font-size: 100px;
      }
      
      .wrapper {
          padding: 20px;
          font-size: 20px;
      }
      
      .wrapper button {
          font-size: 20px;
          background: none;
          border: black solid 1px;
      }
      

      The emoji will need a much larger font-size, since it’s acting as the product image. In addition, you are removing the default gradient background on the button by setting background to none.

      Save and close the file. Now, add the component into the App component to render the Product component in the browser. Open App.js:

      • nano src/components/App/App.js

      Import the component and render it. Also, delete the CSS import since you won’t be using it in this tutorial:

      hooks-tutorial/src/components/App/App.js

      import React from 'react';
      import Product from '../Product/Product';
      
      function App() {
        return <Product />
      }
      
      export default App;
      

      Save and close the file. When you do, the browser will refresh and you’ll see the Product component:

      Product Page

      Now that you have a working component, you can replace the hard-coded data with dynamic values.

      React exports several Hooks that you can import directly from the main React package. By convention, React Hooks start with the word use, such as useState, useContext, and useReducer. Most third-party libraries follow the same convention. For example, Redux has a useSelector and a useStore Hook.

      Hooks are functions that let you run actions as part of the React lifecycle. Hooks are triggered either by other actions or by changes in a component’s props and are used to either create data or to trigger further changes. For example, the useState Hook generates a stateful piece of data along with a function for changing that piece of data and triggering a re-render. It will create a dynamic piece of code and hook into the lifecycle by triggering re-renders when the data changes. In practice, that means you can store dynamic pieces of data in variables using the useState Hook.

      For example, in this component, you have two pieces of data that will change based on user actions: the cart and the total cost. Each of these can be stored in state using the above Hook.

      To try this out, open up Product.js:

      • nano src/components/Product/Product.js

      Next, import the useState Hook from React by adding the highlighted code:

      hooks-tutorial/src/components/Product/Product.js

      import React, { useState } from 'react';
      import './Product.css';
      
      export default function Product() {
        return(
          <div className="wrapper">
            <div>
              Shopping Cart: 0 total items.
            </div>
            <div>Total: 0</div>
      
            <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
            <button>Add</button> <button>Remove</button>
          </div>
        )
      }
      

      useState is a function that takes the initial state as an argument and returns an array with two items. The first item is a variable containing the state, which you will often use in your JSX. The second item in the array is a function that will update the state. Since React returns the data as an array, you can use destructuring to assign the values to any variable names you want. That means you can call useState many times and never have to worry about name conflicts, since you can assign every piece of state and update function to a clearly named variable.

      Create your first Hook by invoking the useState Hook with an empty array. Add in the following highlighted code:

      hooks-tutorial/src/components/Product/Product.js

      import React, { useState } from 'react';
      import './Product.css';
      
      export default function Product() {
        const [cart, setCart] = useState([]);
        return(
          <div className="wrapper">
            <div>
              Shopping Cart: {cart.length} total items.
            </div>
            <div>Total: 0</div>
      
            <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
            <button>Add</button> <button>Remove</button>
          </div>
        )
      }
      

      Here you assigned the first value, the state, to a variable called cart. cart will be an array that contains the products in the cart. By passing an empty array as an argument to useState, you set the initial empty state as the first value of cart.

      In addition to the cart variable, you assigned the update function to a variable called setCart. At this point, you aren’t using the setCart function, and you may see a warning about having an unused variable. Ignore this warning for now; in the next step, you’ll use setCart to update the cart state.

      Save the file. When the browser reloads, you’ll see the page without changes:

      Product Page

      One important difference between Hooks and class-based state management is that, in class-based state management, there is a single state object. With Hooks, state objects are completely independent of each other, so you can have as many state objects as you want. That means that if you want a new piece of stateful data, all you need to do is call useState with a new default and assign the result to new variables.

      Inside Product.js, try this out by creating a new piece of state to hold the total. Set the default value to 0 and assign the value and function to total and setTotal:

      hooks-tutorial/src/components/Product/Product.js

      import React, { useState } from 'react';
      import './Product.css';
      
      export default function Product() {
        const [cart, setCart] = useState([]);
        const [total, setTotal] = useState(0);
        return(
          <div className="wrapper">
            <div>
              Shopping Cart: {cart.length} total items.
            </div>
            <div>Total: {total}</div>
      
            <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
            <button>Add</button> <button>Remove</button>
          </div>
        )
      }
      

      Now that you have some stateful data, you can standardize the displayed data to make a more predictable experience. For example, since the total in this example is a price, it will always have two decimal places. You can use the toLocaleString method to convert total from a number to a string with two decimal places. It will also convert the number to a string according to the numerical conventions that match the browser’s locale. You’ll set the options minimumFractionDigits and maximumFractionDigits to give a consistent number of decimal places.

      Create a function called getTotal. This function will use the in-scope variable total and return a localized string that you will use to display the total. Use undefined as the first argument to toLocaleString to use the system locale rather than specifying a locale:

      hooks-tutorial/src/components/Product/Product.js

      import React, { useState } from 'react';
      import './Product.css';
      
      const currencyOptions = {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2,
      }
      
      export default function Product() {
        const [cart, setCart] = useState([]);
        const [total, setTotal] = useState(0);
      
        function getTotal() {
          return total.toLocaleString(undefined, currencyOptions)
        }
      
        return(
          <div className="wrapper">
            <div>
              Shopping Cart: {cart.length} total items.
            </div>
            <div>Total: {getTotal()}</div>
      
            <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
            <button>Add</button> <button>Remove</button>
          </div>
        )
      }
      

      You now have added some string processing to the displayed total. Even though getTotal is a separate function, it shares the same scope as the surrounding function, which means it can reference the variables of the component.

      Save the file. The page will reload and you’ll see the updated total with two decimal places:

      Price converted to decimal

      This function works, but as of now, getTotal can only operate in this piece of code. In this case, you can convert it to a pure function, which gives the same outputs when given the same inputs and does not rely on a specific environment to operate. By converting the function to a pure function, you make it more reusable. You can, for example, extract it to a separate file and use it in multiple components.

      Update getTotal to take total as an argument. Then move the function outside of the component:

      hooks-tutorial/src/components/Product/Product.js

      import React, { useState } from 'react';
      import './Product.css';
      
      const currencyOptions = {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2,
      }
      
      function getTotal(total) {
        return total.toLocaleString(undefined, currencyOptions)
      }
      
      export default function Product() {
        const [cart, setCart] = useState([]);
        const [total, setTotal] = useState(0);
      
      
        return(
          <div className="wrapper">
            <div>
              Shopping Cart: {cart.length} total items.
            </div>
            <div>Total: {getTotal(total)}</div><^>
      
            <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
            <button>Add</button> <button>Remove</button>
          </div>
        )
      }
      

      Save the file. When you do, the page will reload and you’ll see the component as it was before.

      Functional components like this make it easier to move functions around. As long as there are no scope conflicts, you can move these conversion functions anywhere you want.

      In this step, you set the default value for a stateful piece of data using useState. You then saved the stateful data and a function for updating the state to variables using array destructuring. In the next step, you’ll use the update function to change the state value to re-render the page with updated information.

      Step 2 — Setting State with useState

      In this step, you’ll update your product page by setting a new state with a static value. You have already created the function to update a piece of state, so now you’ll create an event to update both stateful variables with predefined values. By the end of this step, you’ll have a page with state that a user will be able to update at the click of a button.

      Unlike class-based components, you cannot update several pieces of state with a single function call. Instead, you must call each function individually. This means there is a greater separation of concerns, which helps keep stateful objects focused.

      Create a function to add an item to the cart and update the total with the price of the item, then add that functionality to the Add button:

      hooks-tutorial/src/components/Product/Product.js

      import React, { useState } from 'react';
      
      ...
      
      export default function Product() {
        const [cart, setCart] = useState([]);
        const [total, setTotal] = useState(0);
      
        function add() {
          setCart(['ice cream']);
          setTotal(5);
        }
      
        return(
          <div className="wrapper">
            <div>
              Shopping Cart: {cart.length} total items.
            </div>
            <div>Total: {getTotal(total)}</div>
      
            <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
            <button onClick={add}>Add</button><^>
            <button>Remove</button>
          </div>
        )
      }
      

      In this snippet, you called setCart with an array containing the word “ice cream” and called setTotal with 5. You then added this function to the onClick event handler for the Add button.

      Notice that the function must have the same scope as the functions to set state, so it must be defined inside the component function.

      Save the file. When you do, the browser will reload, and when you click on the Add button the cart will update with the current amount:

      Click on the button and see state updated

      Since you are not referencing a this context, you can use either an arrow function or a function declaration. They both work equally well here, and each developer or team can decide which style to use. You can even skip defining an extra function and pass the function directly into the onClick property.

      To try this out, create a function to remove the values by setting the cart to an empty object and the total to 0. Create the function in the onClick prop of the Remove button:

      hooks-tutorial/src/component/Product/Product.js

      import React, { useState } from 'react';
      ...
      export default function Product() {
        const [cart, setCart] = useState([]);
        const [total, setTotal] = useState(0);
      
        function add() {
          setCart(['ice cream']);
          setTotal(5);
        }
      
        return(
          <div className="wrapper">
            <div>
              Shopping Cart: {cart.length} total items.
            </div>
            <div>Total: {getTotal(total)}</div>
      
            <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
            <button onClick={add}>Add</button>
            <button
              onClick={() => {
                setCart([]);
                setTotal(0);
              }}
            >
              Remove
            </button>
          </div>
        )
      }
      

      Save the file. When you do, you will be able to add and remove an item:

      Add and Remove

      Both strategies for assigning the function work, but there are some slight performance implications to creating an arrow function directly in a prop. In every re-render, React will create a new function, which would trigger a prop change and cause the component to re-render. When you define a function outside of a prop, you can take advantage of another Hook called useCallback. This will memoize the function, meaning that it will only create a new function if certain values change. If nothing changes, the program will use the cached memory of the function instead of recalculating it. Some components may not need that level of optimization, but as a rule, the higher a component is likely to be in a tree, the greater the need for memoization.

      In this step, you updated state data with functions created by the useState Hook. You created wrapping functions to call both functions to update the state of several pieces of data at the same time. But these functions are limited because they add static, pre-defined values instead of using the previous state to create the new state. In the next step, you’ll update the state using the current state with both the useState Hook and a new Hook called useReducer.

      Step 3 — Setting State Using Current State

      In the previous step, you updated state with a static value. It didn’t matter what was in the previous state—you always passed the same value. But a typical product page will have many items that you can add to a cart, and you’ll want to be able to update the cart while preserving the previous items.

      In this step, you’ll update the state using the current state. You’ll expand your product page to include several products and you’ll create functions that update the cart and the total based on the current values. To update the values, you’ll use both the useState Hook and a new Hook called useReducer.

      Since React may optimize code by calling actions asynchronously, you’ll want to make sure that your function has access to the most up-to-date state. The most basic way to solve this problem is to pass a function to the state-setting function instead of a value. In other words, instead of calling setState(5), you’d call setState(previous => previous +5).

      To start implementing this, add some more items to the product page by making a products array of objects, then remove the event handlers from the Add and Remove buttons to make room for the refactoring:

      hooks-tutorial/src/component/Product/Product.js

      import React, { useState } from 'react';
      import './Product.css';
      
      ...
      
      const products = [
        {
          emoji: '🍦',
          name: 'ice cream',
          price: 5
        },
        {
          emoji: '🍩',
          name: 'donuts',
          price: 2.5,
        },
        {
          emoji: '🍉',
          name: 'watermelon',
          price: 4
        }
      ];
      
      export default function Product() {
        const [cart, setCart] = useState([]);
        const [total, setTotal] = useState(0);
      
        function add() {
          setCart(['ice cream']);
          setTotal(5);
        }
      
        return(
          <div className="wrapper">
            <div>
              Shopping Cart: {cart.length} total items.
            </div>
            <div>Total: {getTotal(total)}</div>
              <div>
              {products.map(product => (
                <div key={product.name}>
                  <div className="product">
                    <span role="img" aria-label={product.name}>{product.emoji}</span>
                  </div>
                  <button>Add</button>
                  <button>Remove</button>
                </div>
              ))}
            <^></div><^
          </div>
        )
      }
      

      You now have some JSX that uses the .map method to iterate over the array and display the products.

      Save the file. When you do, the page will reload and you’ll see multiple products:

      Product list

      Currently, the buttons have no actions. Since you only want to add the specific product on click, you’ll need to pass the product as an argument to the add function. In the add function, instead of passing the new item directly to the setCart and setTotal functions, you’ll pass an anonymous function that takes the current state and returns a new updated value:

      hooks-tutorial/src/component/Product/Product.js

      import React, { useState } from 'react';
      import './Product.css';
      ...
      export default function Product() {
        const [cart, setCart] = useState([]);
        const [total, setTotal] = useState(0);
      
        function add(product) {
          setCart(current => [...current, product.name]);
          setTotal(current => current + product.price);
        }
      
        return(
          <div className="wrapper">
            <div>
              Shopping Cart: {cart.length} total items.
            </div>
            <div>Total: {getTotal(total)}</div>
      
            <div>
              {products.map(product => (
                <div key={product.name}>
                  <div className="product">
                    <span role="img" aria-label={product.name}>{product.emoji}</span>
                  </div>
                  <button onClick={() => add(product)}>Add</button>
                  <button>Remove</button>
                </div>
              ))}
            </div>
          </div>
        )
      }
      

      The anonymous function uses the most recent state—either cart or total—as an argument that you can use to create a new value. Take care, though, not to directly mutate state. Instead, when adding a new value to the cart you can add the new product to the state by spreading the current value and adding the new value onto the end.

      Save the file. When you do, the browser will reload and you’ll be able to add multiple products:

      Adding products

      There’s another Hook called useReducer that is specially designed to update the state based on the current state, in a manner similar to the .reduce array method. The useReducer Hook is similar to useState, but when you initialize the Hook, you pass in a function the Hook will run when you change the state along with the initial data. The function—referred to as the reducer—takes two arguments: the state and another argument. The other argument is what you will supply when you call the update function.

      Refactor the cart state to use the useReducer Hook. Create a funciton called cartReducer that takes the state and the product as arguments. Replace useState with useReducer, then pass the cartReducer function as the first argument and an empty array as the second argument, which will be the initial data:

      hooks-tutorial/src/component/Product/Product.js

      import React, { useReducer, useState } from 'react';
      
      ...
      
      function cartReducer(state, product) {
        return [...state, product]
      }
      
      export default function Product() {
        const [cart, setCart] = useReducer(cartReducer, []);
        const [total, setTotal] = useState(0);
      
        function add(product) {
          setCart(product.name);
          setTotal(current => current + product.price);
        }
      
        return(
      ...
        )
      }
      

      Now when you call setCart, pass in the product name instead of a function. When you call setCart, you will call the reducer function, and the product will be the second argument. You can make a similar change with the total state.

      Create a function called totalReducer that takes the current state and adds the new amount. Then replace useState with useReducer and pass the new value setCart instead of a function:

      hooks-tutorial/src/component/Product/Product.js

      import React, { useReducer } from 'react';
      
      ...
      
      function totalReducer(state, price) {
        return state + price;
      }
      
      export default function Product() {
        const [cart, setCart] = useReducer(cartReducer, []);
        const [total, setTotal] = useReducer(totalReducer, 0);
      
        function add(product) {
          setCart(product.name);
          setTotal(product.price);
        }
      
        return(
          ...
        )
      }
      

      Since you are no longer using the useState Hook, you removed it from the import.

      Save the file. When you do, the page will reload and you’ll be able to add items to the cart:

      Adding products

      Now it’s time to add the remove function. But this leads to a problem: The reducer functions can handle adding items and updating totals, but it’s not clear how it will be able to handle removing items from the state. A common pattern in reducer functions is to pass an object as the second argument that contains the name of the action and the data for the action. Inside the reducer, you can then update the total based on the action. In this case, you will add items to the cart on an add action and remove them on a remove action.

      Start with the totalReducer. Update the function to take an action as the second argument, then add a conditional to update the state based on the action.type:

      hooks-tutorial/src/component/Product/Product.js

      import React, { useReducer } from 'react';
      import './Product.css';
      
      ...
      
      function totalReducer(state, action) {
        if(action.type === 'add') {
          return state + action.price;
        }
        return state - action.price
      }
      
      export default function Product() {
        const [cart, setCart] = useReducer(cartReducer, []);
        const [total, setTotal] = useReducer(totalReducer, 0);
      
        function add(product) {
          const { name, price } = product;
          setCart(name);
          setTotal({ price, type: 'add' });
        }
      
        return(
          ...
        )
      }
      

      The action is an object with two properites: type and price. The type can be either add or remove, and the price is a number. If the type is add, it increases the total. If it is remove, it lowers the total. After updating the totalReducer, you call setTotal with a type of add and the price, which you set using destructuring assignment.

      Next, you will update the cartReducer. This one is a little more complicated: You can use if/then conditionals, but it’s more common to use a switch statement. Switch statements are particularly useful if you have a reducer that can handle many different actions because it makes those actions more readable in your code.

      As with the totalReducer, you’ll pass an object as the second item type and name properties. If the action is remove, update the state by splicing out the first instance of a product.

      After updating the cartReducer, create a remove function that calls setCart and setTotal with objects containing type: 'remove' and either the price or the name. Then use a switch statement to update the data based on the action type. Be sure to return the final state:

      hooks-tutorial/src/complicated/Product/Product.js

      import React, { useReducer } from 'react';
      import './Product.css';
      
      ...
      
      function cartReducer(state, action) {
        switch(action.type) {
          case 'add':
            return [...state, action.name];
          case 'remove':
            const update = [...state];
            update.splice(update.indexOf(action.name), 1);
            return update;
          default:
            return state;
        }
      }
      
      function totalReducer(state, action) {
        if(action.type === 'add') {
          return state + action.price;
        }
        return state - action.price
      }
      
      export default function Product() {
        const [cart, setCart] = useReducer(cartReducer, []);
        const [total, setTotal] = useReducer(totalReducer, 0);
      
        function add(product) {
          const { name, price } = product;
          setCart({ name, type: 'add' });
          setTotal({ price, type: 'add' });
        }
      
        function remove(product) {
          const { name, price } = product;
          setCart({ name, type: 'remove' });
          setTotal({ price, type: 'remove' });
        }
      
        return(
          <div className="wrapper">
            <div>
              Shopping Cart: {cart.length} total items.
            </div>
            <div>Total: {getTotal(total)}</div>
      
            <div>
              {products.map(product => (
                <div key={product.name}>
                  <div className="product">
                    <span role="img" aria-label={product.name}>{product.emoji}</span>
                  </div>
                  <button onClick={() => add(product)}>Add</button>
                  <button onClick={() => remove(product)}>Remove</button>
                </div>
              ))}
            </div>
          </div>
        )
      }
      

      As you work on your code, take care not to directly mutate the state in the reducer functions. Instead, make a copy before splicing out the object. Also note it is a best practice to add a default action on a switch statement in order to account for unforeseen edge cases. In this, case just return the object. Other options for the default are throwing an error or falling back to an action such as add or remove.

      After making the changes, save the file. When the browser refreshes, you’ll be able to add and remove items:

      Remove items

      There is still a subtle bug left in this product. In the remove method, you can subtract from a price even if the item is not in the cart. If you click Remove on the ice cream without adding it to your cart, your displayed total will be -5.00.

      You can fix this bug by checking that an item exists before you subtract it, but a more efficient way is to minimize the different pieces of state by only saving related data in one place. In other words, try to avoid double references to the same data, in this case, the product. Instead, store the raw data in one state variable—the whole product object—then perform the calculations using that data.

      Refactor the component so that the add() function passes the whole product to the reducer and the remove() function removes the whole object. The getTotal method will use the cart, and so you can delete the totalReducer function. Then you can pass the cart to getTotal(), which you can refactor to reduce the array to a single value:

      hooks-tutorial/src/component/Product/Product.js

      import React, { useReducer } from 'react';
      import './Product.css';
      
      const currencyOptions = {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2,
      }
      
      function getTotal(cart) {
        const total = cart.reduce((totalCost, item) => totalCost + item.price, 0);
        return total.toLocaleString(undefined, currencyOptions)
      }
      
      ...
      
      function cartReducer(state, action) {
        switch(action.type) {
          case 'add':
            return [...state, action.product];
          case 'remove':
            const productIndex = state.findIndex(item => item.name === action.product.name);
            if(productIndex < 0) {
              return state;
            }
            const update = [...state];
            update.splice(productIndex, 1)
            return update
          default:
            return state;
        }
      }
      
      export default function Product() {
        const [cart, setCart] = useReducer(cartReducer, []);
      
        function add(product) {
          setCart({ product, type: 'add' });
        }
      
        function remove(product) {
          setCart({ product, type: 'remove' });
        } 
      
        return(
          <div className="wrapper">
            <div>
              Shopping Cart: {cart.length} total items.
            </div>
            <div>Total: {getTotal(cart)}</div>
      
            <div>
              {products.map(product => (
                <div key={product.name}>
                  <div className="product">
                    <span role="img" aria-label={product.name}>{product.emoji}</span>
                  </div>
                  <button onClick={() => add(product)}>Add</button>
                  <button onClick={() => remove(product)}>Remove</button>
                </div>
              ))}
            </div>
          </div>
        )
      }
      

      Save the file. When you do, the browser will refresh and you’ll have your final cart:

      Add and remove products

      By using the useReducer Hook, you kept your main component body well-organized and legible, since the complex logic for parsing and splicing the array is outside of the component. You also could move the reducer outside the componet if you wanted to reuse it, or you can create a custom Hook to use across multiple components. You can make custom Hooks as functions surrounding basic Hooks, such as useState, useReducer, or useEffect.

      Hooks give you the chance to move the stateful logic in and out of the component, as opposed to classes, where you are generally bound to the component. This advantage can extend to other components as well. Since Hooks are functions, you can import them into multiple components rather then using inheritance or other complex forms of class composition.

      In this step, you learned to set state using the current state. You created a component that updated state using both the useState and the useReducer Hooks, and you refactored the component to different Hooks to prevent bugs and improve reusability.

      Conclusion

      Hooks were a major change to React that created a new way to share logic and update components without using classes. Now that you can create components using useState and useReducer, you have the tools to make complex projects that respond to users and dynamic information. You also have a foundation of knowledge that you can use to explore more complex Hooks or to create custom Hooks.

      If you would like to look at more React tutorials, check out our React Topic page, or return to the How To Code in React.js series page.



      Source link

      How To Manage State on React Class Components


      The author selected Creative Commons to receive a donation as part of the Write for DOnations program.

      Introduction

      In React, state refers to a structure that keeps track of how data changes over time in your application. Managing state is a crucial skill in React because it allows you to make interactive components and dynamic web applications. State is used for everything from tracking form inputs to capturing dynamic data from an API. In this tutorial, you’ll run through an example of managing state on class-based components.

      As of the writing of this tutorial, the official React documentation encourages developers to adopt React Hooks to manage state with functional components when writing new code, rather than using class-based components. Although the use of React Hooks is considered a more modern practice, it’s important to understand how to manage state on class-based components as well. Learning the concepts behind state management will help you navigate and troubleshoot class-based state management in existing code bases and help you decide when class-based state management is more appropriate. There’s also a class-based method called componentDidCatch that is not available in Hooks and will require setting state using class methods.

      This tutorial will first show you how to set state using a static value, which is useful for cases where the next state does not depend on the first state, such as setting data from an API that overrides old values. Then it will run through how to set a state as the current state, which is useful when the next state depends on the current state, such as toggling a value. To explore these different ways of setting state, you’ll create a product page component that you’ll update by adding purchases from a list of options.

      Prerequisites

      Step 1 — Creating an Empty Project

      In this step, you’ll create a new project using Create React App. Then you will delete the sample project and related files that are installed when you bootstrap the project. Finally, you will create a simple file structure to organize your components. This will give you a solid basis on which to build this tutorial’s sample application for managing state on class-based components.

      To start, make a new project. In your terminal, run the following script to install a fresh project using create-react-app:

      • npx create-react-app state-class-tutorial

      After the project is finished, change into the directory:

      In a new terminal tab or window, start the project using the Create React App start script. The browser will auto-refresh on changes, so leave this script running while you work:

      You will get a running local server. If the project did not open in a browser window, you can open it with http://localhost:3000/. If you are running this from a remote server, the address will be http://your_domain:3000.

      Your browser will load with a simple React application included as part of Create React App:

      React template project

      You will be building a completely new set of custom components, so you’ll need to start by clearing out some boilerplate code so that you can have an empty project.

      To start, open src/App.js in a text editor. This is the root component that is injected into the page. All components will start from here. You can find more information about App.js at How To Set Up a React Project with Create React App.

      Open src/App.js with the following command:

      You will see a file like this:

      state-class-tutorial/src/App.js

      import React from 'react';
      import logo from './logo.svg';
      import './App.css';
      
      function App() {
        return (
          <div className="App">
            <header className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <p>
                Edit <code>src/App.js</code> and save to reload.
              </p>
              <a
                className="App-link"
                href="https://reactjs.org"
                target="_blank"
                rel="noopener noreferrer"
              >
                Learn React
              </a>
            </header>
          </div>
        );
      }
      
      export default App;
      

      Delete the line import logo from './logo.svg';. Then replace everything in the return statement to return a set of empty tags: <></>. This will give you a valid page that returns nothing. The final code will look like this:

      state-class-tutorial/src/App.js

      
      import React from 'react';
      import './App.css';
      
      function App() {
        return <></>;
      }
      
      export default App;
      

      Save and exit the text editor.

      Finally, delete the logo. You won’t be using it in your application and you should remove unused files as you work. It will save you from confusion in the long run.

      In the terminal window type the following command:

      If you look at your browser, you will see a blank screen.

      blank screen in chrome

      Now that you have cleared out the sample Create React App project, create a simple file structure. This will help you keep your components isolated and independent.

      Create a directory called components in the src directory. This will hold all of your custom components.

      Each component will have its own directory to store the component file along with the styles, images, and tests.

      Create a directory for App:

      Move all of the App files into that directory. Use the wildcard, *, to select any files that start with App. regardless of file extension. Then use the mv command to put them into the new directory:

      • mv src/App.* src/components/App

      Next, update the relative import path in index.js, which is the root component that bootstraps the whole process:

      The import statement needs to point to the App.js file in the App directory, so make the following highlighted change:

      state-class-tutorial/src/index.js

      import React from 'react';
      import ReactDOM from 'react-dom';
      import './index.css';
      import App from './components/App/App';
      import * as serviceWorker from './serviceWorker';
      
      ReactDOM.render(
        <React.StrictMode>
          <App />
        </React.StrictMode>,
        document.getElementById('root')
      );
      
      // If you want your app to work offline and load faster, you can change
      // unregister() to register() below. Note this comes with some pitfalls.
      // Learn more about service workers: https://bit.ly/CRA-PWA
      serviceWorker.unregister();
      

      Save and exit the file.

      Now that the project is set up, you can create your first component.

      Step 2 — Using State in a Component

      In this step, you’ll set the initial state of a component on its class and reference the state to display a value. You’ll then make a product page with a shopping cart that displays the total items in the cart using the state value. By the end of the step, you’ll know the different ways to hold a value and when you should use state rather than a prop or a static value.

      Building the Components

      Start by creating a directory for Product:

      • mkdir src/components/Product

      Next, open up Product.js in that directory:

      • nano src/components/Product/Product.js

      Start by creating a component with no state. The component will have two parts: The cart, which has the number of items and the total price, and the product, which has a button to add and remove an item. For now, the buttons will have no actions.

      Add the following code to Product.js:

      state-class-tutorial/src/components/Product/Product.js

      import React, { Component } from 'react';
      import './Product.css';
      
      export default class Product extends Component {
        render() {
          return(
            <div className="wrapper">
              <div>
                Shopping Cart: 0 total items.
              </div>
              <div>Total: 0</div>
      
              <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
              <button>Add</button> <button>Remove</button>
            </div>
          )
        }
      }
      

      You have also included a couple of div elements that have JSX class names so you can add some basic styling.

      Save and close the file, then open Product.css:

      • nano src/components/Product/Product.css

      Give some light styling to increase the font-size for the text and the emoji:

      state-class-tutorial/src/components/Product/Product.css

      .product span {
          font-size: 100px;
      }
      
      .wrapper {
          padding: 20px;
          font-size: 20px;
      }
      
      .wrapper button {
          font-size: 20px;
          background: none;
      }
      

      The emoji will need a much larger font size than the text, since it’s acting as the product image in this example. In addition, you are removing the default gradient background on buttons by setting the background to none.

      Save and close the file.

      Now, render the Product component in the App component so you can see the results in the browser. Open App.js:

      • nano src/components/App/App.js

      Import the component and render it. You can also delete the CSS import since you won’t be using it in this tutorial:

      state-class-tutorial/src/components/App/App.js

      import React from 'react';
      import Product from '../Product/Product';
      
      function App() {
        return <Product />
      }
      
      export default App;
      

      Save and close the file. When you do, the browser will refresh and you’ll see the Product component.

      Product Page

      Setting the Initial State on a Class Component

      There are two values in your component values that are going to change in your display: total number of items and total cost. Instead of hard coding them, in this step you’ll move them into an object called state.

      The state of a React class is a special property that controls the rendering of a page. When you change the state, React knows that the component is out-of-date and will automatically re-render. When a component re-renders, it modifies the rendered output to include the most up-to-date information in state. In this example, the component will re-render whenever you add a product to the cart or remove it from the cart. You can add other properties to a React class, but they won’t have the same ability to trigger re-rendering.

      Open Product.js:

      • nano src/components/Product/Product.js

      Add a property called state to the Product class. Then add two values to the state object: cart and total. The cart will be an array, since it may eventually hold many items. The total will be a number. After assigning these, replace references to the values with this.state.property:

      state-class-tutorial/src/components/Product/Product.js

      
      import React, { Component } from 'react';
      import './Product.css';
      
      export default class Product extends Component {
      
        state = {
          cart: [],
          total: 0
        }
      
        render() {
          return(
            <div className="wrapper">
              <div>
                Shopping Cart: {this.state.cart.length} total items.
              </div>
              <div>Total {this.state.total}</div>
      
              <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
              <button>Add</button> <button>Remove</button>
            </div>
          )
        }
      }
      

      Notice that in both cases, since you are referencing JavaScript inside of your JSX, you need to wrap the code in curly braces. You are also displaying the length of the cart array to get a count of the number of items in the array.

      Save the file. When you do, the browser will refresh and you’ll see the same page as before.

      Product Page

      The state property is a standard class property, which means that it is accessible in other methods, not just the render method.

      Next, instead of displaying the price as a static value, convert it to a string using the toLocaleString method, which will convert the number to a string that matches the way numbers are displayed in the browser’s region.

      Create a method called getTotal() that takes the state and converts it to a localized string using an array of currencyOptions. Then, replace the reference to state in the JSX with a method call:

      state-class-tutorial/src/components/Product/Product.js

      import React, { Component } from 'react';
      import './Product.css';
      
      export default class Product extends Component {
      
        state = {
          cart: [],
          total: 0
        }
      
        currencyOptions = {
          minimumFractionDigits: 2,
          maximumFractionDigits: 2,
        }
      
        getTotal = () => {
          return this.state.total.toLocaleString(undefined, this.currencyOptions)
        }
      
        render() {
          return(
            <div className="wrapper">
              <div>
                Shopping Cart: {this.state.cart.length} total items.
              </div>
              <div>Total {this.getTotal()}</div>
      
              <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
              <button>Add</button> <button>Remove</button>
            </div>
          )
        }
      }
      

      Since total is a price for goods, you are passing currencyOptions that set the maximum and minimum decimal places for your total to two. Note that this is set as a separate property. Often, beginner React developers will put information like this in the state object, but it is best to only add information to state that you expect to change. This way, the information in state will be easier to keep strack of as your application scales.

      Another important change you made was to create the getTotal() method by assigning an arrow function to a class property. Without using the arrow function, this method would create a new this binding, which would interfere with the current this binding and introduce a bug into our code. You’ll see more on this in the next step.

      Save the file. When you do, the page will refresh and you’ll see the value converted to a decimal.

      Price converted to decimal

      You’ve now added state to a component and referenced it in your class. You also accessed values in the render method and in other class methods. Next, you’ll create methods to update the state and show dynamic values.

      Step 3 — Setting State from a Static Value

      So far you’ve created a base state for the component and you’ve referenced that state in your functions and your JSX code. In this step, you’ll update your product page to modify the state on button clicks. You’ll learn how to pass a new object containing updated values to a special method called setState, which will then set the state with the updated data.

      To update state, React developers use a special method called setState that is inherited from the base Component class. The setState method can take either an object or a function as the first argument. If you have a static value that doesn’t need to reference the state, it’s best to pass an object containing the new value, since it’s easier to read. If you need to reference the current state, you pass a function to avoid any references to out-of-date state.

      Start by adding an event to the buttons. If your user clicks Add, then the program will add the item to the cart and update the total. If they click Remove, it will reset the cart to an empty array and the total to 0. For example purposes, the program will not allow a user to add an item more then once.

      Open Product.js:

      • nano src/components/Product/Product.js

      Inside the component, create a new method called add, then pass the method to the onClick prop for the Add button:

      state-class-tutorial/src/components/Product/Product.js

      import React, { Component } from 'react';
      import './Product.css';
      
      export default class Product extends Component {
      
        state = {
          cart: [],
          total: 0
        }
      
        add = () => {
          this.setState({
            cart: ['ice cream'],
            total: 5
          })
        }
      
        currencyOptions = {
          minimumFractionDigits: 2,
          maximumFractionDigits: 2,
        }
      
        getTotal = () => {
          return this.state.total.toLocaleString(undefined, this.currencyOptions)
        }
      
        render() {
          return(
            <div className="wrapper">
              <div>
                Shopping Cart: {this.state.cart.length} total items.
              </div>
              <div>Total {this.getTotal()}</div>
      
              <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
              <button onClick={this.add}>Add</button>
              <button>Remove</button>
            </div>
          )
        }
      }
      

      Inside the add method, you call the setState method and pass an object containing the updated cart with a single item ice cream and the updated price of 5. Notice that you again used an arrow function to create the add method. As mentioned before, this will ensure the function has the proper this context when running the update. If you add the function as a method without using the arrow function, the setState would not exist without binding the function to the current context.

      For example, if you created the add function this way:

      export default class Product extends Component {
      ...
        add() {
          this.setState({
            cart: ['ice cream'],
            total: 5
          })
        }
      ...
      }
      

      The user would get an error when they click on the Add button.

      Context Error

      Using an arrow function ensures that you’ll have the proper context to avoid this error.

      Save the file. When you do, the browser will reload, and when you click on the Add button the cart will update with the current amount.

      Click on the button and see state updated

      With the add method, you passed both properties of the state object: cart and total. However, you do not always need to pass a complete object. You only need to pass an object containing the properties that you want to update, and everything else will stay the same.

      To see how React can handle a smaller object, create a new function called remove. Pass a new object containing just the cart with an empty array, then add the method to the onClick property of the Remove button:

      state-class-tutorial/src/components/Product/Product.js

      import React, { Component } from 'react';
      import './Product.css';
      
      export default class Product extends Component {
      
        ...
        remove = () => {
          this.setState({
            cart: []
          })
        }
      
        render() {
          return(
            <div className="wrapper">
              <div>
                Shopping Cart: {this.state.cart.length} total items.
              </div>
              <div>Total {this.getTotal()}</div>
      
              <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
              <button onClick={this.add}>Add</button>
              <button onClick={this.remove}>Remove</button>
            </div>
          )
        }
      }
      

      Save the file. When the browser refreshes, click on the Add and Remove buttons. You’ll see the cart update, but not the price. The total state value is preserved during the update. This value is only preserved for example purposes; with this application, you would want to update both properties of the state object. But you will often have components with stateful properties that have different responsibilities, and you can make them persist by leaving them out of the updated object.

      The change in this step was static. You knew exactly what the values would be ahead of time, and they didn’t need to be recalculated from state. But if the product page had many products and you wanted to be able to add them multiple times, passing a static object would provide no guarantee of referencing the most up-to-date state, even if your object used a this.state value. In this case, you could instead use a function.

      In the next step, you’ll update state using functions that reference the current state.

      Step 4 — Setting State Using Current State

      There are many times when you’ll need to reference a previous state to update a current state, such as updating an array, adding a number, or modifying an object. To be as accurate as possible, you need to reference the most up-to-date state object. Unlike updating state with a predefined value, in this step you’ll pass a function to the setState method, which will take the current state as an argument. Using this method, you will update a component’s state using the current state.

      Another benefit of setting state with a function is increased reliability. To improve performance, React may batch setState calls, which means that this.state.value may not be fully reliable. For example, if you update state quickly in several places, it is possible that a value could be out of date. This can happen during data fetches, form validations, or any situation where several actions are occurring in parallel. But using a function with the most up-to-date state as the argument ensures that this bug will not enter your code.

      To demonstrate this form of state management, add some more items to the product page. First, open the Product.js file:

      • nano src/components/Product/Product.js

      Next, create an array of objects for different products. The array will contain the product emoji, name, and price. Then loop over the array to display each product with an Add and Remove button:

      state-class-tutorial/src/components/Product/Product.js

      import React, { Component } from 'react';
      import './Product.css';
      
      const products = [
        {
          emoji: '🍦',
          name: 'ice cream',
          price: 5
        },
        {
          emoji: '🍩',
          name: 'donuts',
          price: 2.5,
        },
        {
          emoji: '🍉',
          name: 'watermelon',
          price: 4
        }
      ];
      
      export default class Product extends Component {
      
        ...
      
      
        render() {
          return(
            <div className="wrapper">
              <div>
                Shopping Cart: {this.state.cart.length} total items.
              </div>
              <div>Total {this.getTotal()}</div>
              <div>
                {products.map(product => (
                  <div key={product.name}>
                    <div className="product">
                      <span role="img" aria-label={product.name}>{product.emoji}</span>
                    </div>
                    <button onClick={this.add}>Add</button>
                    <button onClick={this.remove}>Remove</button>
                  </div>
                ))}
              </div>
            </div>
          )
        }
      }
      

      In this code, you are using the map() array method to loop over the products array and return the JSX that will display each element in your browser.

      Save the file. When the browser reloads, you’ll see an updated product list:

      Product list

      Now you need to update your methods. First, change the add() method to take the product as an argument. Then instead of passing an object to setState(), pass a function that takes the state as an argument and returns an object that has the cart updated with the new product and the total updated with the new price:

      state-class-tutorial/src/components/Product/Product.js

      import React, { Component } from 'react';
      import './Product.css';
      
      ...
      
      export default class Product extends Component {
      
        state = {
          cart: [],
          total: 0
        }
      
        add = (product) => {
          this.setState(state => ({
            cart: [...state.cart, product.name],
            total: state.total + product.price
          }))
        }
      
        currencyOptions = {
          minimumFractionDigits: 2,
          maximumFractionDigits: 2,
        }
      
        getTotal = () => {
          return this.state.total.toLocaleString(undefined, this.currencyOptions)
        }
      
        remove = () => {
          this.setState({
            cart: []
          })
        }
      
        render() {
          return(
            <div className="wrapper">
              <div>
                Shopping Cart: {this.state.cart.length} total items.
              </div>
              <div>Total {this.getTotal()}</div>
      
              <div>
                {products.map(product => (
                  <div key={product.name}>
                    <div className="product">
                      <span role="img" aria-label={product.name}>{product.emoji}</span>
                    </div>
                    <button onClick={() => this.add(product)}>Add</button>
                    <button onClick={this.remove}>Remove</button>
                  </div>
                ))}
              </div>
            </div>
          )
        }
      }
      

      Inside the anonymous function that you pass to setState(), make sure you reference the argument—state—and not the component’s state—this.state. Otherwise, you still run a risk of getting an out-of-date state object. The state in your function will be otherwise identical.

      Take care not to directly mutate state. Instead, when adding a new value to the cart, you can add the new product to the state by using the spread syntax on the current value and adding the new value onto the end.

      Finally, update the call to this.add by changing the onClick() prop to take an anonymous function that calls this.add() with the relevant product.

      Save the file. When you do, the browser will reload and you’ll be able to add multiple products.

      Adding products

      Next, update the remove() method. Follow the same steps: convert setState to take a function, update the values without mutating, and update the onChange() prop:

      state-class-tutorial/src/components/Product/Product.js

      import React, { Component } from 'react';
      import './Product.css';
      
      ...
      
      export default class Product extends Component {
      
      ...
      
        remove = (product) => {
          this.setState(state => {
            const cart = [...state.cart];
            cart.splice(cart.indexOf(product.name))
            return ({
              cart,
              total: state.total - product.price
            })
          })
        }
      
        render() {
          return(
            <div className="wrapper">
              <div>
                Shopping Cart: {this.state.cart.length} total items.
              </div>
              <div>Total {this.getTotal()}</div>
              <div>
                {products.map(product => (
                  <div key={product.name}>
                    <div className="product">
                      <span role="img" aria-label={product.name}>{product.emoji}</span>
                    </div>
                    <button onClick={() => this.add(product)}>Add</button>
                    <button onClick={() => this.remove(product)}>Remove</button>
                  </div>
                ))}
              </div>
            </div>
          )
        }
      }
      

      To avoid mutating the state object, you must first make a copy of it using the spread operator. Then you can splice out the item you want from the copy and return the copy in the new object. By copying state as the first step, you can be sure that you will not mutate the state object.

      Save the file. When you do, the browser will refresh and you’ll be able to add and remove items:

      Remove items

      There is still a bug in this application: In the remove method, a user can subtract from the total even if the item is not in the cart. If you click Remove on the ice cream without adding it to your cart, your total will be -5.00.

      You can fix the bug by checking for an item’s existence before subtracting, but an easier way is to keep your state object small by only keeping references to the products and not separating references to products and total cost. Try to avoid double references to the same data. Instead, store the raw data in state— in this case the whole product object—then perform the calculations outside of the state.

      Refactor the component so that the add() method adds the whole object, the remove() method removes the whole object, and the getTotal method uses the cart:

      state-class-tutorial/src/components/Product/Product.js

      import React, { Component } from 'react';
      import './Product.css';
      
      ...
      
      export default class Product extends Component {
      
        state = {
          cart: [],
        }
      
        add = (product) => {
          this.setState(state => ({
            cart: [...state.cart, product],
          }))
        }
      
        currencyOptions = {
          minimumFractionDigits: 2,
          maximumFractionDigits: 2,
        }
      
        getTotal = () => {
          const total = this.state.cart.reduce((totalCost, item) => totalCost + item.price, 0);
          return total.toLocaleString(undefined, this.currencyOptions)
        }
      
        remove = (product) => {
          this.setState(state => {
            const cart = [...state.cart];
            const productIndex = cart.findIndex(p => p.name === product.name);
            if(productIndex < 0) {
              return;
            }
            cart.splice(productIndex, 1)
            return ({
              cart
            })
          })
        }
      
        render() {
          ...
        }
      }
      

      The add() method is similar to what it was before, except that reference to the total property has been removed. In the remove() method, you find the index of the product with findByIndex. If the index doesn’t exist, you’ll get a -1. In that case, you use a conditional statement toreturn nothing. By returning nothing, React will know the state didn’t change and won’t trigger a re-render. If you return state or an empty object, it will still trigger a re-render.

      When using the splice() method, you are now passing 1 as the second argument, which will remove one value and keep the rest.

      Finally, you calculate the total using the reduce() array method.

      Save the file. When you do, the browser will refresh and you’ll have your final cart:

      Add and remove

      The setState function you pass can have an additional argument of the current props, which can be helpful if you have state that needs to reference the current props. You can also pass a callback function to setState as the second argument, regardless of if you pass an object or function for the first argument. This is particularly useful when you are setting state after fetching data from an API and you need to perform a new action after the state update is complete.

      In this step, you learned how to update a new state based on the current state. You passed a function to the setState function and calculated new values without mutating the current state. You also learned how to exit a setState function if there is no update in a manner that will prevent a re-render, adding a slight performance enhancement.

      Conclusion

      In this tutorial, you have developed a class-based component with a dynamic state that you’ve updated statically and using the current state. You now have the tools to make complex projects that respond to users and dynamic information.

      React does have a way to manage state with Hooks, but it is helpful to understand how to use state on components if you need to work with components that must be class-based, such as those that use the componentDidCatch method.

      Managing state is key to nearly all components and is necessary for creating interactive applications. With this knowledge you can recreate many common web components, such as sliders, accordions, forms, and more. You will then use the same concepts as you build applications using hooks or develop components that pull data dynamically from APIs.

      If you would like to look at more React tutorials, check out our React Topic page, or return to the How To Code in React.js series page.



      Source link

      Creating Single File Components in VueJS – A Tutorial


      Updated by Linode Contributed by Pavel Petrov

      When first learning VueJS, and when using it for smaller projects, you will likely use regular, globally-defined components. Once your project grows and you start needing more structure and flexibility, single file components can be a better option.

      Below you can see an example of a barebones single file component, which we will examine part-by-part later in the guide:

      SkeletonComponent.vue
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      
      <template>
      <h1>{{ greeting }}</h1>
      </template>
      
      <script>
      export default {
          name: 'SkeletonComponent',
          data: function() {
              return {
                  greeting: 'Hello'
              };
          },
          props: [],
          methods: {
          },
          created: function(){
          }
      }
      </script>
      
      <style scoped>
      h1 {
          font-size: 2em;
          text-align: center;
      }
      </style>

      In this guide, you will learn:

      Note

      Before You Begin

      If you haven’t read our Building and Using VueJS Components already, go take a look.

      Make sure you have Node.js installed. If you don’t, our How to Install Node.js guide outlines different installation options.

      What are Single File Components

      Single file components are similar to regular components, but there are a few key differences which can make single file components the better tool for your project:

      • They can be defined locally, instead of globally.

      • You can define your component’s <template> outside of your JavaScript, which allows for syntax highlighting in your text editor, unlike with string templates.

      • CSS/styling information is included in the component definition.

      Inspecting a Single File Component

      Single file components are contained in files with the .vue extension. Each .vue file consists of three parts: template, script, style. Let’s revisit our barebones component:

      SkeletonComponent.vue
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      
      <template>
      <h1>{{ greeting }}</h1>
      </template>
      
      <script>
      export default {
          name: 'SkeletonComponent',
          data: function() {
              return {
                  greeting: 'Hello'
              };
          },
          props: [],
          methods: {
          },
          created: function(){
          }
      }
      </script>
      
      <style scoped>
      h1 {
          font-size: 2em;
          text-align: center;
      }
      </style>
      • Lines 1-3 of the component define the <template>, where we specify the HTML template of our component. In comparison, a regular component’s template is represented with a string property inside the component’s JavaScript. This can become increasingly confusing for complex components, because there is no syntax highlighting within the string.

        Another benefit for your templates is that you do not have to adjust the {{ }} mustache tag delimiters to [[ ]] or something else if you are working with another framework that already uses them.

        Note

        For example, Symfony developers using VueJS would have to update their delimiter configuration, because Twig already uses mustache delimiters for its rendering methods. Even though this might be a fairly trivial task, using single file components eliminates that need entirely.
      • The script section of the component (lines 5-19) defines the component’s properties and business logic. This is similar to how regular components are defined, but instead everything is within an export statement.

      • The style section, on lines 21-26, uses the scoped attribute to create component-specific CSS. If you were instead using regular components, you would have no way of adding component-specific CSS, and thus you would have to define your styles globally.

        This makes your components completely independent, so you can now not only use them in your current project, but reuse them among other projects as well. Finally, you can use preprocessors like SASS and Babel for the styling information in your component.

      Prepare your Development Environment

      One drawback of single file components for beginners is that they require webpack or Browserify to build. These tools bundle your application’s dependencies, but they can add to the learning curve. Vue provides a CLI package that’s built on top of webpack and which simplifies managing your project.

      We’ll use this tool throughout this guide; to install it, run:

      sudo npm install -g @vue/cli
      

      The Vue CLI will now be available globally on your workstation (because the -g flag was used).

      Note

      If you’re using NVM, you can install Vue CLI without sudo:

      npm install -g @vue/cli
      

      Create your Project

      All of the examples in this guide will live under a single project. Run the vue create command to create a directory for this project and have Vue CLI build the project skeleton for you:

      vue create single-file-components --packageManager=npm
      
        
      Vue CLI v4.3.1
      ? Please pick a preset: (Use arrow keys)
      ❯ default (babel, eslint)
      Manually select features
      
      

      Note

      You can specify --packageManager=yarn if you prefer yarn to npm.

      The CLI uses pretty sensible defaults, so if you’re a beginner you can just press enter and the Vue CLI will build your first project and install the needed dependencies. If you haven’t done this before, it might take a while to fetch the needed dependencies.

      Now let’s test:

      cd single-file-components && npm run serve
      
        
      DONE Compiled successfully in 3398ms
      
      App running at:
      
      -   Local: http://localhost:8080/
      -   Network: unavailable
      
      Note that the development build is not optimized.
      To create a production build, run npm run build.
      
      

      What npm run serve does is run the development server, but the cool thing is that while you make changes the dev server automatically rebuilds the project and injects the changes in the browser, so you don’t even have to refresh.

      Now, if everything is fine, you should be able to open http://localhost:8080 in your browser and you will see the VueJS welcome screen:

      VueJS Welcome Screen

      Let’s look at the directory structure of the default application and go through each folder:

      tree -I node_modules
      
        
      .
      ├── babel.config.js
      ├── package.json
      ├── package-lock.json
      ├── public
      │   ├── favicon.ico
      │   └── index.html
      ├── README.md
      └── src
          ├── App.vue
          ├── assets
          │   └── logo.png
          ├── components
          │   └── HelloWorld.vue
          └── main.js
      
      

      Note

      The -I node_modules option will tell tree to ignore your node_modules/ directory, which is where all of the node dependencies reside.

      The public Folder and index.html

      Files in the public folder will not be bundled by webpack. When your project is created, this folder will contain an index.html file:

      index.html
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="utf-8" />
          <meta http-equiv="X-UA-Compatible" content="IE=edge" />
          <meta name="viewport" content="width=device-width,initial-scale=1.0" />
          <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
          <title><%= htmlWebpackPlugin.options.title %></title>
      </head>
      <body>
          <noscript>
              <strong>
                  We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't
                  work properly without JavaScript enabled. Please enable it to
                  continue.
              </strong>
          </noscript>
          <div id="app"></div>
          <!-- built files will be auto injected -->
      </body>
      </html>

      On lines 7, 8, and 13 you will notice the <%= %> syntax where the favicon link and page title are embedded; this is part of the lodash template syntax, which the index file is written in. While your index file isn’t included in webpack’s dependency bundle, it will be processed by the html-webpack-plugin, which does a few useful things:

      • It populates the variables that you embed using the template syntax. You can see more about the default variable values exposed by webpack here.
      • It automatically connects your index to the app bundle that webpack compiles: on line 19, you’ll see a comment that says the files built by webpack are auto-injected by the build procedure.

        More about the build procedure for index.html

        This is an example of what the file will look like after the build procedure:

        index.html
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        
        <!DOCTYPE html>
        <html lang=en>
        <head>
            <meta charset=utf-8>
            <meta http-equiv=X-UA-Compatible content="IE=edge">
            <meta name=viewport content="width=device-width,initial-scale=1">
            <link rel=icon href=/favicon.ico> <title>single-file-components</title>
            <link href=/css/app.fb0c6e1c.css rel=preload as=style>
            <link href=/js/app.ae3090b2.js rel=preload as=script>
            <link href=/js/chunk-vendors.b4c61135.js rel=preload as=script>
            <link href=/css/app.fb0c6e1c.css rel=stylesheet>
        </head>
        <body>
            <noscript>
                <strong>
                    We're sorry but single-file-components doesn't work properly without JavaScript enabled. Please
                    enable it to continue.
                </strong>
            </noscript>
            <div id=app></div>
            <script src=/js/chunk-vendors.b4c61135.js></script>
            <script src=/js/app.ae3090b2.js></script>
        </body>
        </html>

        Notice that your app’s script and CSS dependencies have been added to the file on lines 21 and 22, and that these files have random hash appended their names (e.g. app.ae3090b2.js). These hashes will change over time for subsequent builds of your app, and the html-webpack-plugin will keep the hash updated in your index. Without this feature, you would need to update those lines for each build.

      The rest of the body contains these elements:

      • The noscript tag, which is in place to warn users with disabled JS that the app will not work unless they enable it.
      • The <div id="app"></div> container where our VueJS app will be bound.

      The src Folder

      The src/ folder is where most of your work will be done. The src/main.js file will serve as the entry point for webpack’s build process:

      src/main.js
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      import Vue from 'vue'
      import App from './App.vue'
      
      Vue.config.productionTip = false
      
      new Vue({
          render: h => h(App),
      }).$mount('#app')
      

      This file imports VueJS (line 1), imports the App component from the src folder (line 2), and binds the App component to the container with the id property set to app (lines 6-8).

      Now to the interesting part: src/App.vue:

      src/App.vue
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      
      <template>
          <div id="app">
              <img alt="Vue logo" src="./assets/logo.png" />
              <HelloWorld msg="Welcome to Your Vue.js App" />
          </div>
      </template>
      
      <script>
      import HelloWorld from "./components/HelloWorld.vue";
      export default {
          name: "App",
          components: {
              HelloWorld,
          },
      };
      </script>
      
      <style>
      #app {
          font-family: Avenir, Helvetica, Arial, sans-serif;
          -webkit-font-smoothing: antialiased;
          -moz-osx-font-smoothing: grayscale;
          text-align: center;
          color: #2c3e50;
          margin-top: 60px;
      }
      </style>

      This is a simple single file component relatively similar to the example we discussed above, but this example shows how to import and use components:

      • On line 9, the HelloWorld component is imported.
      • On lines 12-14, the HelloWorld component is locally registered for use within the App component. The registered component can only be used in the template of the parent component that registered it. Contrast this with the components in Building and Using VueJS Components, which were globally registered.

        Note

        Local registration is a valuable architectural feature for reusable components within big projects.

      • The HelloWorld component is used within the App component’s template on line 4.

      Building your First Single File Components

      Now that we’ve covered the basic structure of the project created by Vue CLI, let’s build our own components on top of that. As in Building and Using VueJS Components, we will again be building a rating application, but this time it will be a little more sophisticated.

      This is what your rating app will look like:

      Rating App - Finished Product

      This is how it will behave:

      • Clicking on a star on the left side will register a vote for that star.

      • The left side will interactively change when a user hovers over the stars.

      • It will allow the user to rate only once on each visit to the page. If the page is refreshed, or if it is visited again later, the user can vote again.

      • It will keep score of votes between page visits in the browser’s local storage.

      Here’s how the app’s template will look in protocode; you do not need to copy and paste this:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      
      <div id="app">
          <div class="inner">
              <div class="ratingContainer">
                  <span class="bigRating"></span>
                  <div class="rating-stars">
                      <Star weight="1"></Star>
                      <Star weight="2"></Star>
                      <Star weight="3"></Star>
                      <Star weight="4"></Star>
                      <Star weight="5"></Star>
                  </div>
              </div>
              <Summary></Summary>
          </div>
      </div>

      We’ll make each star a separate component (named Star), and we’ll also create a Summary component which will hold the summary of the votes.

      App.vue

      To start, replace the content of your App.vue with this snippet:

      src/App.vue
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      
      <template>
          <div id="app">
              <div class="inner">
                  <div class="ratingContainer">
                      <span class="bigRating" v-html="bigRating"></span>
                      <div>
                          <Star
                              v-for="index in 5"
                              v-bind:key="index"
                              v-bind:weight="index"
                              v-bind:enabled="enabled"
                              v-bind:currentRating="currentRating"
                          ></Star>
                      </div>
                  </div>
                  <Summary v-bind:ratings="ratings"></Summary>
              </div>
          </div>
      </template>
      
      <script>
      import Star from "./components/Star.vue";
      import Summary from "./components/Summary.vue";
      
      export default {
          name: "App",
          components: { Star, Summary },
          data: function () {
              return {
                  currentRating: 0,
                  bigRating: "&#128566;", // Emoji: 😶
                  enabled: true,
                  ratings: [
                      {
                          weight: 1,
                          votes: 0,
                      },
                      {
                          weight: 2,
                          votes: 0,
                      },
                      {
                          weight: 3,
                          votes: 0,
                      },
                      {
                          weight: 4,
                          votes: 0,
                      },
                      {
                          weight: 5,
                          votes: 0,
                      },
                  ],
              };
          },
          methods: {},
          created: function () {
              if (localStorage.ratings) {
                  this.ratings = JSON.parse(localStorage.ratings);
              }
          },
      };
      </script>
      
      <style>
      @import url(https://fonts.googleapis.com/css?family=Roboto:100, 300, 400);
      @import url(https://netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css);
      #app {
          width: 400px;
      }
      .ratingContainer {
          float: left;
          width: 45%;
          margin-right: 5%;
          text-align: center;
      }
      .bigRating {
          color: #333333;
          font-size: 72px;
          font-weight: 100;
          line-height: 1em;
          padding-left: 0.1em;
      }
      </style>

      This is the main component, but there are no methods set on it yet, so for now it doesn’t have any functionality. Here are some notable parts of the code:

      • <template>:

        • On lines 7-13, all five Star components are rendered from a single <Star> declaration with the v-for="index in 5" syntax. A weight is assigned to each Star by the v-bind:weight="index" syntax. The key attribute is also bound to the index. The enabled and currentRating props will be bound to values that are described in the <script> section.

          Note

          The v-for syntax is similar to the following for loop: for(let index=1;index<=5;index++).

        • On line 16, the Summary component is rendered. It will display data from the bound ratings property.

      • <script>

        • Lines 22 and 23 import the Star and Summary components, which are then registered on line 27. These will be created separately in the next section.

        • The data function is declared on lines 28-56, and it contains the following variables which will control the functionality of the app once the methods are added later:

          • currentRating: As we hover over the stars, we will use this variable to store the rating of the hovered star.

          • bigRating: This will be set to an emoticon that represents the currentRating.

          • enabled: This will be used to disable the rating application once the user has cast a vote.

          • ratings: This is a structure for the votes that have been cast. We set the default value in the data function, and if there are any votes saved in the browser’s localStorage, then we overwrite the defaults, which imitates a persistence layer. In the created hook (lines 58-62) you can see how we fetch the saved cast votes.

      Star.vue and Summary.vue

      In your src/components/ directory, create two files named Star.vue and Summary.vue and paste these snippets into them:

      src/components/Star.vue
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      <template>
          <i class="icon-star"></i>
      </template>
      
      <script>
      export default {
          name: "Star",
          props: ["weight", "enabled", "currentRating"]
      };
      </script>
      
      <style scoped>
      i.icon-star {
          font-size: 20px;
          color: #e3e3e3;
          margin-bottom: 0.5em;
      }
      </style>
      src/components/Summary.vue
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      
      <template>
          <div class="summaryContainer">
              <ul>
                  <li v-for="rating in ratings" v-bind:key="rating.weight">
                      {{ rating.weight }}<i class="icon-star"></i>: {{ rating.votes }}
                  </li>
              </ul>
          </div>
      </template>
      
      <script>
      export default {
          name: "Summary",
          props: ["ratings"]
      };
      </script>
      
      <style scoped>
      .summaryContainer {
          float: left;
          width: 50%;
          font-size: 13px;
      }
      </style>

      Here are some notable parts of the code:

      • In both components, the Font Awesome icon-star is used. On lines 13-17 of Star.vue, some styling is set for the icons in the Star component, including setting the color to light grey.

        Because this style section uses the scoped attribute, these styles are limited to the Star component. As a result, the icons in the Summary component are not also styled in this way.

      • On lines 4-6 of Summary.vue, the v-for syntax is used again to display the rating votes.

      After creating Star.vue and Summary.vue, the application can be viewed in the browser. Head to http://127.0.0.1:8080 and you will see the following:

      Rating App - No Votes, Noninteractive

      Because there are no methods set on the components yet, it will not be interactive.

      Note

      If you’re not still running npm run serve in your terminal, you’ll need to re-run it from inside your project.

      Adding Methods to the Components

      The application right now is a skeleton, so now we’ll make it work. These three custom events will be handled:

      • When you hover over a star, all previous stars will be highlighted in yellow. For example, if you hover over the star number 4, stars 1-3 also get highlighted.

      • When your mouse moves away, the highlight will be removed.

      • When you click on a star, a vote is cast and you no longer can vote until you visit the page again.

      Updating App.vue

      1. Update the Star component declaration in the <template> of src/App.vue to match this snippet:

        src/App.vue
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        
        <!-- ... --->
        <Star
            v-for="index in 5"
            v-bind:key="index"
            v-bind:weight="index"
            v-bind:enabled="enabled"
            v-bind:currentRating="currentRating"
            v-on:lightUp="lightUpHandler"
            v-on:lightDown="lightDownHandler"
            v-on:rate="rateHandler"
        ></Star>
        <!-- ... --->

        The new additions to this declaration are the v-on directives, which set methods as event handlers for the custom lightUp, lightDown, and rate events.

        Note

        The Star component will be updated in the next section to emit those events.

      2. Next, replace the methods object in the component with the following snippet. These are the event handlers:

        src/App.vue
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        
        // ...
        methods: {
            lightUpHandler: function (weight) {
                this.currentRating = weight;
        
                // Display different emojis based on the weight
                if (weight <= 2) {
                    this.bigRating = "&#128549;"; // Emoji: 😥
                }
                if (weight > 2 && weight <= 4) {
                    this.bigRating = "&#128556;"; // Emoji: 😬
                }
                if (weight > 4) {
                    this.bigRating = "&#128579;"; // Emoji: 🙃
                }
            },
            lightDownHandler: function () {
                // Reset on mouse away
                this.currentRating = 0;
                this.bigRating = "&#128566;"; // Emoji: 😶
            },
            rateHandler: function (weight) {
                this.currentRating = weight;
        
                // Finding the relevant rating and incrementing the cast votes
                let rating = this.ratings.find((obj) => obj.weight == weight);
                rating.votes++;
        
                // Disabling from voting again
                this.enabled = false;
        
                // Saves the votes to the browser localStorage
                localStorage.setItem("ratings", JSON.stringify(this.ratings));
            },
        },
        // ...
        
        • The lightUpHandler and rateHandler methods receive a weight from the Star component that emitted the corresponding event. These methods set the weight as the currentRating.

        • At the end of the rateHandler method, the component’s ratings are converted to a JSON object and saved so we can use them as a starting point the next time the page loads (line 33).

        Full contents of App.vue

        At this point, your App.vue should be the same as this snippet:

        src/App.vue
          1
          2
          3
          4
          5
          6
          7
          8
          9
         10
         11
         12
         13
         14
         15
         16
         17
         18
         19
         20
         21
         22
         23
         24
         25
         26
         27
         28
         29
         30
         31
         32
         33
         34
         35
         36
         37
         38
         39
         40
         41
         42
         43
         44
         45
         46
         47
         48
         49
         50
         51
         52
         53
         54
         55
         56
         57
         58
         59
         60
         61
         62
         63
         64
         65
         66
         67
         68
         69
         70
         71
         72
         73
         74
         75
         76
         77
         78
         79
         80
         81
         82
         83
         84
         85
         86
         87
         88
         89
         90
         91
         92
         93
         94
         95
         96
         97
         98
         99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        111
        112
        113
        114
        115
        116
        117
        118
        119
        120
        121
        
        <template>
            <div id="app">
                <div class="inner">
                    <div class="ratingContainer">
                        <span class="bigRating" v-html="bigRating"></span>
                        <div>
                            <Star
                                v-for="index in 5"
                                v-bind:key="index"
                                v-bind:weight="index"
                                v-bind:enabled="enabled"
                                v-bind:currentRating="currentRating"
                                v-on:lightUp="lightUpHandler"
                                v-on:lightDown="lightDownHandler"
                                v-on:rate="rateHandler"
                            ></Star>
                        </div>
                    </div>
                    <Summary v-bind:ratings="ratings"></Summary>
                </div>
            </div>
        </template>
        
        <script>
        import Star from "./components/Star.vue";
        import Summary from "./components/Summary.vue";
        
        export default {
            name: "App",
            components: { Star, Summary },
            data: function () {
                return {
                    currentRating: 0,
                    bigRating: "&#128566;", // Emoji: 😶
                    enabled: true,
                    ratings: [
                        {
                            weight: 1,
                            votes: 0,
                        },
                        {
                            weight: 2,
                            votes: 0,
                        },
                        {
                            weight: 3,
                            votes: 0,
                        },
                        {
                            weight: 4,
                            votes: 0,
                        },
                        {
                            weight: 5,
                            votes: 0,
                        },
                    ],
                };
            },
            methods: {
                lightUpHandler: function (weight) {
                    this.currentRating = weight;
        
                    // Display different emojis based on the weight
                    if (weight <= 2) {
                        this.bigRating = "&#128549;"; // Emoji: 😥
                    }
                    if (weight > 2 && weight <= 4) {
                        this.bigRating = "&#128556;"; // Emoji: 😬
                    }
                    if (weight > 4) {
                        this.bigRating = "&#128579;"; // Emoji: 🙃
                    }
                },
                lightDownHandler: function () {
                    // Reset on mouse away
                    this.currentRating = 0;
                    this.bigRating = "&#128566;"; // Emoji: 😶
                },
                rateHandler: function (weight) {
                    this.currentRating = weight;
        
                    // Finding the relevant rating and incrementing the cast votes
                    let rating = this.ratings.find((obj) => obj.weight == weight);
                    rating.votes++;
        
                    // Disabling from voting again
                    this.enabled = false;
        
                    // Saves the votes to the browser localStorage
                    localStorage.setItem("ratings", JSON.stringify(this.ratings));
                },
            },
            created: function () {
                if (localStorage.ratings) {
                    this.ratings = JSON.parse(localStorage.ratings);
                }
            },
        };
        </script>
        
        <style>
        @import url(https://fonts.googleapis.com/css?family=Roboto:100, 300, 400);
        @import url(https://netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css);
        #app {
            width: 400px;
        }
        .ratingContainer {
            float: left;
            width: 45%;
            margin-right: 5%;
            text-align: center;
        }
        .bigRating {
            color: #333333;
            font-size: 72px;
            font-weight: 100;
            line-height: 1em;
            padding-left: 0.1em;
        }
        </style>

      Updating Star.vue

      Let’s modify the Star component to emit the events:

      1. In the template of Star.vue, replace the <i> element with this snippet:

        src/components/Star.vue
        1
        2
        3
        4
        5
        6
        7
        8
        
        <!-- ... --->
        <i
            v-bind:class="getClass()"
            v-on:mouseover="mouseoverHandler"
            v-on:mouseleave="mouseleaveHandler"
            v-on:click="clickHandler"
        ></i>
        <!-- ... --->
        • The CSS classes of the icon will now be dynamically generated by a getClass method on the component. This change is made so that the hover highlight effect can be toggled by a CSS class.

        • The mouseover, mouseleave, and click DOM events are associated with new handler methods that will also be added to the component.

      2. In the script section, add this data function to the component:

        src/components/Star.vue
        1
        2
        3
        4
        5
        6
        7
        8
        
        // ...
        data: function () {
            return {
                hover: false,
            };
        },
        // ...
        

        The hover variable will maintain the hover state of the component.

      3. Also in the script section, add this methods object to the component:

        src/components/Star.vue
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        
        // ...
        methods: {
            getClass: function () {
                var baseClass = "icon-star";
        
                // Adds the hover class if you're hovering over the component or you are hovering over a star with greater weight
                if (this.hover || this.currentRating >= this.weight) {
                    baseClass += " hover";
                }
                return baseClass;
            },
            mouseoverHandler: function () {
                // Makes sure stars are not lighting up after vote is cast
                if (this.enabled) {
                    // Emits the lightUp event with the weight as a parameter
                    this.$emit("lightUp", this.weight);
                    // Enables hover class
                    this.hover = true;
                }
            },
            mouseleaveHandler: function () {
                // Makes sure stars are not lighting up after vote is cast
                if (this.enabled) {
                    // Emits the lightDown event
                    this.$emit("lightDown", this.weight);
                    // Removes hover class
                    this.hover = false;
                }
            },
            clickHandler: function () {
                // Makes sure you only vote if you haven't voted yet
                if (this.enabled) {
                    // Emits the rate event with the weight as parameter
                    this.$emit("rate", this.weight);
                } else {
                    alert("Already voted");
                }
            },
        },
        // ...
        
        • The mouseoverHandler, mouseleaveHandler, and clickHandler methods will emit the lightUp, lightDown, and rate custom events, respectively.

        • These methods also first check to see if enabled has been set to false; if false, then the methods do nothing, which means that the DOM events will result in no action.

        • In the getClass method, the currentRating prop is used to determine if a star icon should be highlighted. This prop was previously bound to the currentRating data property of the App component.

          Note

          The currentRating prop is not a particularly beautiful solution, but we will improve on that further in the guide.

      4. Finally, add this rule to the style section:

        src/components/Star.vue
        1
        2
        3
        4
        5
        6
        
        /* ... */
        i.icon-star.hover {
            color: yellow;
        }
        /* ... */
        

        Full contents of Star.vue

        At this point, your Star.vue should be the same as this snippet:

        src/components/Star.vue
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        
        <template>
            <i
                v-bind:class="getClass()"
                v-on:mouseover="mouseoverHandler()"
                v-on:mouseleave="mouseleaveHandler()"
                v-on:click="clickHandler()"
            ></i>
        </template>
        
        <script>
        export default {
            name: "Star",
            data: function () {
                return {
                    hover: false,
                };
            },
            props: ["weight", "enabled", "currentRating"],
            methods: {
                getClass: function () {
                    var baseClass = "icon-star";
        
                    // Adds the hover class if you're hovering over the component or you are hovering over a star with greater weight
                    if (this.hover || this.currentRating >= this.weight) {
                        baseClass += " hover";
                    }
                    return baseClass;
                },
                mouseoverHandler: function () {
                    // Makes sure stars are not lighting up after vote is cast
                    if (this.enabled) {
                        // Emits the lightUp event with the weight as a parameter
                        this.$emit("lightUp", this.weight);
                        // Enables hover class
                        this.hover = true;
                    }
                },
                mouseleaveHandler: function () {
                    // Makes sure stars are not lighting up after vote is cast
                    if (this.enabled) {
                        // Emits the lightDown event
                        this.$emit("lightDown", this.weight);
                        // Removes hover class
                        this.hover = false;
                    }
                },
                clickHandler: function () {
                    // Makes sure you only vote if you haven't voted yet
                    if (this.enabled) {
                        // Emits the rate event with the weight as parameter
                        this.$emit("rate", this.weight);
                    } else {
                        alert("Already voted");
                    }
                },
            },
        };
        </script>
        
        <style scoped>
        i.icon-star {
            font-size: 20px;
            color: #e3e3e3;
            margin-bottom: 0.5em;
        }
        i.icon-star.hover {
            color: yellow;
        }
        </style>
      5. Head to http://localhost:8080/ in your browser, and you should see that your rating application now works. Try hovering over the stars and clicking on them to observe the interaction. If you refresh the page, you can vote again, and the votes will be tallied:

      Rating App - With Rating Interaction

      Communication between Components Via an Event Bus

      Notice how clumsy all of the v-on directives chained one after the other look:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      
      <Star
          v-for="index in 5"
          v-bind:key="index"
          v-bind:weight="index"
          v-bind:enabled="enabled"
          v-bind:currentRating="currentRating"
          v-on:lightUp="lightUpHandler"
          v-on:lightDown="lightDownHandler"
          v-on:rate="rateHandler"
      ></Star>

      This setup can be inelegant to scale: imagine having 10 of those on a single component, and then imagine you have 10 components. The directives would become hard to follow, so it’s worth exploring other ways to communicate between components.

      Fortunately, VueJS supports a publish-subscribe pattern called an event bus. You can easily implement it in your components to make things a bit more elegant.

      Event Bus Basics

      In VueJS, an event bus is a new Vue instance that is declared globally (in main.js, for example):

      src/main.js
      1
      2
      3
      4
      
      // ...
      export const eventBus = new Vue();
      // ...
      

      It is then imported in each component which accesses it:

      AnyComponent.vue
      1
      2
      3
      4
      
      // ...
      import { eventBus } from "../main.js";
      // ...
      

      Components can emit events to the event bus:

      SomeComponent.vue
      1
      2
      3
      4
      
      // ...
      eventBus.$emit("event", parameter);
      // ...
      

      Other components will register event handlers on the same event bus with the $on method:

      AnotherComponent.vue
      1
      2
      3
      4
      5
      6
      
      // ...
      eventBus.$on("event", (parameter) => {
          // Do stuff
      });
      // ...
      

      Basically, think of the event bus as a global communication layer between your components.

      Adding an Event Bus to your App

      Now let’s rebuild our example to take advantage of an event bus:

      1. Open main.js and replace its content with this snippet:

        src/main.js
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        
        import Vue from "vue";
        import App from "./App.vue";
        
        Vue.config.productionTip = false;
        
        export const eventBus = new Vue();
        
        new Vue({
            render: h => h(App)
        }).$mount("#app");
        

        This update adds an event bus declaration on line 6.

      2. Open App.vue and replace its content with this snippet:

        src/App.vue
          1
          2
          3
          4
          5
          6
          7
          8
          9
         10
         11
         12
         13
         14
         15
         16
         17
         18
         19
         20
         21
         22
         23
         24
         25
         26
         27
         28
         29
         30
         31
         32
         33
         34
         35
         36
         37
         38
         39
         40
         41
         42
         43
         44
         45
         46
         47
         48
         49
         50
         51
         52
         53
         54
         55
         56
         57
         58
         59
         60
         61
         62
         63
         64
         65
         66
         67
         68
         69
         70
         71
         72
         73
         74
         75
         76
         77
         78
         79
         80
         81
         82
         83
         84
         85
         86
         87
         88
         89
         90
         91
         92
         93
         94
         95
         96
         97
         98
         99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        
        <template>
            <div id="app">
                <div class="inner">
                    <div class="ratingContainer">
                        <span class="bigRating" v-html="bigRating"></span>
                        <div>
                            <Star
                                v-for="index in 5"
                                v-bind:key="index"
                                v-bind:weight="index"
                                v-bind:enabled="enabled"
                            ></Star>
                        </div>
                    </div>
                    <Summary v-bind:ratings="ratings"></Summary>
                </div>
            </div>
        </template>
        
        <script>
        import Star from "./components/Star.vue";
        import Summary from "./components/Summary.vue";
        
        import { eventBus } from "./main.js";
        
        export default {
            name: "App",
            components: { Star, Summary },
            data: function () {
                return {
                    bigRating: "&#128566;", // Emoji: 😶
                    enabled: true,
                    ratings: [
                        {
                            weight: 1,
                            votes: 0,
                        },
                        {
                            weight: 2,
                            votes: 0,
                        },
                        {
                            weight: 3,
                            votes: 0,
                        },
                        {
                            weight: 4,
                            votes: 0,
                        },
                        {
                            weight: 5,
                            votes: 0,
                        },
                    ],
                };
            },
            created: function () {
                if (localStorage.ratings) {
                    this.ratings = JSON.parse(localStorage.ratings);
                }
                eventBus.$on("lightUp", (weight) => {
                    // Display different emojis based on the weight
                    if (weight <= 2) {
                        this.bigRating = "&#128549;"; // Emoji: 😥
                    }
                    if (weight > 2 && weight <= 4) {
                        this.bigRating = "&#128556;"; // Emoji: 😬
                    }
                    if (weight > 4) {
                        this.bigRating = "&#128579;"; // Emoji: 🙃
                    }
                });
                eventBus.$on("lightDown", () => {
                    this.bigRating = "&#128566;"; // Emoji: 😶
                });
                eventBus.$on("rate", (weight) => {
                    // Finding the relevant rating and incrementing the cast votes
                    let rating = this.ratings.find((obj) => obj.weight == weight);
                    rating.votes++;
        
                    // Disabling from voting again
                    this.enabled = false;
        
                    // Saves the votes to the browser localStorage
                    localStorage.setItem("ratings", JSON.stringify(this.ratings));
                });
            },
        };
        </script>
        
        <style>
        @import url(https://fonts.googleapis.com/css?family=Roboto:100, 300, 400);
        @import url(https://netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css);
        #app {
            width: 400px;
        }
        .ratingContainer {
            float: left;
            width: 45%;
            margin-right: 5%;
            text-align: center;
        }
        .bigRating {
            color: #333333;
            font-size: 72px;
            font-weight: 100;
            line-height: 1em;
            padding-left: 0.1em;
        }
        </style>

        The following changes have been made in this updated file:

        • The eventBus instance is imported on line 24.
        • We removed the v-on directives from the Star component declaration in the template (lines 7-12).
        • The component’s methods have been removed, which previously served as the event handlers for the v-on directives.
        • Instead, we subscribe to the events in the created hook (lines 61-86). The logic that was in the component’s methods has been moved here.
        • We also no longer need the currentRating data property, so it has been removed. This is because the Star components will also subscribe to the event bus and can be directly notified of all lightUp and rate events.

        The template looks much leaner now, and you can easily spot the subscribed events by simply having a look in the created hook.

      3. Open Star.vue and replace its content with this snippet:

        src/components/Star.vue
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89
        
        <template>
            <i
                v-bind:class="getClass()"
                v-on:mouseover="mouseoverHandler"
                v-on:mouseleave="mouseleaveHandler"
                v-on:click="clickHandler"
            ></i>
        </template>
        
        <script>
        import { eventBus } from "../main.js";
        
        export default {
            name: "Star",
            data: function () {
                return {
                    hover: false,
                    active: false,
                };
            },
            props: ["weight", "enabled"],
            methods: {
                getClass: function () {
                    var baseClass = "icon-star";
                    if (this.active) {
                        baseClass += " active";
                    }
                    if (this.hover) {
                        baseClass += " hover";
                    }
                    return baseClass;
                },
                mouseoverHandler: function () {
                    // Makes sure stars are not lighting up after vote is cast
                    if (this.enabled) {
                        // Emits the lightUp event with the weight as a parameter
                        eventBus.$emit("lightUp", this.weight);
                    }
                },
                mouseleaveHandler: function () {
                    // Makes sure stars are not lighting up after vote is cast
                    if (this.enabled) {
                        // Emits the lightDown event
                        eventBus.$emit("lightDown");
                    }
                },
                clickHandler: function () {
                    // Makes sure you only vote if you haven't voted yet
                    if (this.enabled) {
                        // Emits the rate event with the weight as parameter
                        eventBus.$emit("rate", this.weight);
                    } else {
                        alert("Already voted");
                    }
                },
            },
            created: function () {
                eventBus.$on("lightUp", (targetWeight) => {
                    if (targetWeight >= this.weight) {
                        this.hover = true;
                    } else {
                        this.hover = false;
                    }
                });
                eventBus.$on("lightDown", () => {
                    this.hover = false;
                });
                eventBus.$on("rate", (targetWeight) => {
                    if (targetWeight >= this.weight) {
                        this.active = true;
                    }
                });
            },
        };
        </script>
        
        <style scoped>
        i.icon-star {
            font-size: 20px;
            color: #e3e3e3;
            margin-bottom: 0.5em;
        }
        i.icon-star.hover {
            color: yellow;
        }
        i.icon-star.active {
            color: #737373;
        }
        </style>

        The following changes have been made in this updated file:

        • The eventBus instance is imported on line 11.
        • The currentRating prop has been removed (line 21).
        • We’ve modified the handler methods to emit the events on the eventBus instance (lines 22-56)
        • We also subscribe to the same events from the created hook (lines 57-73), so that all Star components are aware of which component the user is currently hovering over without needing the currentRating prop.
        • We’ve added an active class to the component’s style (lines 86-88). This is enabled when a user enters a rating, and it sets a different highlight color for the stars. To enable the class, an active data property has been added to the component (line 18), and it is set to true within the rate event handling logic (line 70).

        Rating App - With Event Bus

      More Information

      You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.

      This guide is published under a CC BY-ND 4.0 license.



      Source link