Translate

Поиск по этому блогу

пятница, 10 мая 2019 г.

React хуки и функциональные компоненты.

Сегодня мы будем использовать самые современные практики React и создадим простое приложение, полностью на функциональных компонентах, с использованием хуков.



Чтобы не утомлять вас долгими рассказами о том. что это за приложение, я скажу так, что это поиск рецептов, которые мы получаем со стороннего сайта. Лучше одни раз увидеть - здесь.

Нам потребуется сайт, который предоставит нам данные. Для этой цели мы будем использовать edamam.com. Для использования этого сервиса вам потребуется регистрация. Она простая. На сайте переходите на вкладку "API Developer Portal". Далее, вверху слева, выбираете "APIs" и в выпадающем меню - "Recipe Search API". Все, как показано на картинке, ниже.



На вкладке "Developer", выбираете поле нажимаете - "start now".



выбираете имя пользователя, пароль, вводите адрес почты и ...все попадаете на вкладку "Dashboard" вашего приложения, где нас будут интересовать только два поля: Application ID и Application Keys .



Нам потребуется пример запроса. Для этого переходим в раздел "Документы" разработчиков и в середине страницы находим пример дляGET запроса:

curl "https://api.edamam.com/search?q=chicken&app_id=${YOUR_APP_ID}&app_key=${YOUR_APP_KEY}&from=0&to=3&calories=591-722&health=alcohol-free"

Все как на картинке ниже:



Скопируем его полностью.

Теперь, развернем наше приложение на основе create-react-app. После обновления до третей версии, это приложение перестало запускаться "из коробки" полностью, из-за проблем со скриптами. Для того, чтобы не переустанавливать все после создания приложения я использую свою заготовку — cra3-boilerplate. Там я достаточно подробно описал, как все это развернуть на рабочем компе.

Производим обычные изменения в файле App.js переписываем компонент на функцию и удаляем все внутри метода render(), именно внутри div className="App". Напишем вывод простой форму с импутом и кнопокйо отправки запроса для поиска. Зададим сразу классы, для стилизации элементов. Удаляем logo и его импорт.


<form className="search-form">
  <input className="search-bar" type="text" />
  <button className="search-button" type="submit">
    Search
  </button>
</form>;



Внутри функции, создадим три константы (APP_ID, APP_KEY и exampleRequest), куда и запишем данные нашего приложения, полученные на сейте рецептов ранее.

Для того, чтобы лучше понять, как работают некоторые хуки в реакт, мы сделаем небольшое отступление. Вы можете это просто причитать, для сведения.

Коротко о работе useEffect


Представим. что у нас в приложении есть некий счетчик кликов. Он подключен вне формы, ну например - к заголовку в который и будем выводить данные этого счетчика. Счетчик будет меняться по клику.

<h1 onClick={() => setCounter(counter + 1)}>{counter}</h1>

Для сохранения данных в приложении, мы будем использовать хук - useState с начальным значением равным нулю.

const [counter, setCounter] = useState(0);

Посмотрите в браузер и убедитесь, что все работает.

Теперь, добавим в наше приложение еще один полезный хук - useEffect. С помощью этого хука мы будем отправлять запросы на сервер, как если бы мы использовали жизненный цикл — componentDidMount компонента с состоянием

Этот хук принимает функцию. В ней мы просто выведем в консоль сообщение, что данный хук запущен.

  useEffect(() => {
    console.log("useEffect has been run");
  });



Теперь, если посмотреть в консоль приложения, то мы увидим, что этот хук запускается по каждому клику на счетчик.



Для того, чтобы задать ему правильную работу, мы можем передать в функцию этого хука пустой массив, и получим работу useEffect, только при первом построении нашего приложения, то есть практически тоже самое. что componentDidMount.



На фото видно, что не смотря на то, что мы кликнули на счетчик уже десяток раз, но хук запустился только однажды - при загрузке документа.

Если мы передадим туда переменную, которую мы хотим отслеживать, то мы можем просто написать так:

  useEffect(() => {
    console.log("useEffect has been run");
  }, [counter]);



Таким образом, запуск этого эффекта будет сопровождать каждое изменение значения счетчика — counter, а не любой другой переменной данного приложения.

import React from "react";
import "./App.css";

const App = () => {
  const APP_ID = "yourData Application-ID";
  const APP_KEY = "yourData Application-Kyes";
  const exampleRequest = `https://api.edamam.com/search?q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}`;

  const [counter, setCounter] = useState(0);

  useEffect(() => {
    console.log("useEffect has been run");
  }, [counter]);
  return (
    <div className="App">
      <form className="search-form">
        <input className="search-bar" type="text" />
        <button className="search-button" type="submit">
          Search
        </button>
      </form>
      <h1 onClick={() => setCounter(counter + 1)}>{counter}</h1>
    </div>
  );
};
export default App;


Все изменения на этом этапе, вы можете посмотреть в моем репозитории на gitHub — react-recipe-edamam
ci -m "Counter with useEffect"
ci -m - здесь и всегда в этом блоге обозначают коммит.
Вернемся в наше приложение и продолжим.

Получение данных и сохранение их в переменную

На этом этапе нашей задачей будет отправить запрос к сайту, который нам предоставил API, получить их и сохранить в "состояние". Для этого нам потребуется хук "состояния" - useState, который мы импортируем из реакт.

Для запроса к серверу, мы отойдем от обычного использования обещаний - Promuse, а воспользуемся более удобным способом придания асинхронности запросу, с помощью - async / await

Об этом я много раз писал в этом блоге и вы можете найти посты перейдя по ссылке. async / await в этом блоге.


Для сохранения данных, которые мы получим, будем использовать хук состояния - useState

const [recipes, setRecipes] = useState([]);

Забегая немного вперед, я скажу. что данные нам приходят в виде массива объектов. В этом легко убедиться. если просто вывести их вначале в консоль. Но я, зная это, сразу создам эти переменные "состояния". В обычной практике, все таки стоит убедиться в этом при получении данных.

Для отправки запроса создадим отдельную функцию - getRecipes

const getRecipes = async () => {
  const response = await fetch(
    `https://api.edamam.com/search?q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}`
  );
  const data = await response.json()
  setRecipes(data)
  console.log(data)
}


Ничего особенно нового. Для запроса, мы использовали строку, которую ранее сохраняли в переменную - exampleRequest. И сделали просто асинхронное получение данных, сохранение их в переменную "состояния" setRecipes(data) и вывод их в консоль.

Для отправки запроса на сервер, мы будем использовать хук - useEffect, куда и передадим нашу функцию и пустой массив, для исключения повторных запросов к серверу.

Кстати, бесплатный лимит на запросы на этом сервисе составляет 10 запросов в минуту.


useEffect(() => {
  getRecipes()
},[])





Здесь мы видим, что к нам пришли данные в виде массива объектов.

Каждый отдельный элемент содержит следующие поля:



А все рецепты приходят внутри поля hits



Потому мы можем смело сохранять не поле data, а data.hits. И тогда в консоли мы увидим:

Здесь я приведу полностью файл App.js на этом этапе:

App.js
import React, { useEffect, useState } from "react";
import "./App.css";

const App = () => {
  const APP_ID = "your_data_app_ID";
  const APP_KEY = "your_data_app_keys";

  useEffect(() => {
    getRecipes();
  }, []);

  const getRecipes = async () => {
    const response = await fetch(
      `https://api.edamam.com/search?q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}`
    );
    const data = await response.json();
    console.log(data.hits);
  };
  return (
    <div className="App">
      <form className="search-form">
        <input className="search-bar" type="text" />
        <button className="search-button" type="submit">
          Search
        </button>
      </form>
    </div>
  );
};

export default App;





Все изменения на этом этапе, вы можете посмотреть в моем репозитории на gitHub — react-recipe-edamam
ci -m "getRecipes with async / await fetch request and output in console"


Создание компонента для вывода отдельного элемента

Коль скоро, данные у нас получены и сохранены, то мы можем их вывести на страницу. Для этого создадим отдельный компонент - Recipes.js.

Это будет тоже простой функциональный компонент, в который мы передадим данные, и задачей которого будет вывод данных на страницу.

Ниже, я приведу его полностью, так легче объяснять.

Recipes.js

import React from "react";

const Recipes = ({ title, calories, image }) => {
  return (
    <div>
      <h1>{title}</h1>
      <p>{calories}</p>
      <img src={image} alt="" />
    </div>
  );
};
export default Recipes;




Здесь мы, путем деструктуризации, получаем из props передаваемые значения, сразу же выводя их на страницу с помощью компонента.

Для стилизации добавили классы.

Это конечная версия нашего компонента, но на этапе его создания, я просто выводил статические данные, и не принимал props. Получилось то, что вы можете видеть на картинке ниже.



Да, и не забудьте импортировать ваш вновь созданный компонент, в App.js

import Recipes from './Recipes';

И ниже формы в рендере, выведем с помощью метода map, те данные, которые мы получили и сохранили в переменной recipes

      <div className="recipes">
        {recipes.map(recipe => (
          <Recipes
            key={recipe.recipe.label}
            title={recipe.recipe.label}
            calories={recipe.recipe.calories}
            image={recipe.recipe.image}
          />
        ))}
      </div>;


В этом коде я уже передал нужные данные компоненту, но на этапе создания я этого не делал и потому получал простой вывод статического компонента N-ное количество раз.

Еще важно отметить необходимость создания ключа для вывода компонентов списком. Споры о ключах еще ведутся в сообществе, но для них мы можем использовать любые статические данные, которые будут помогать определять реакту нужный элемент в дереве компонентов. На этот раз я использовал key={recipe.recipe.label}

На картинке выше, вы видите. что компонент вывелся столько же раз, сколько пришло элементов с данными.

Если посмотреть в консоль, то можно четко увидеть структуру, полученных данных и определиться с возможностью их вывода на страницу (передачи в компонент Recipes.js )



Как только мы определились с данными и решили, что мы будем выводить в компоненте, то (код выше) мы получим следующую картину в браузере:



Таким образом, мы получили данные и вывели их на страницу.

Все изменения на этом этапе, вы можете посмотреть в моем репозитории на gitHub — react-recipe-edamam
ci -m "output of recipes to the page"


Создание поиска элементов для запроса с сервера.

В данный момент у нас в запросе передается строка, где просто прописано - chicken

  const response = await fetch(
    `https://api.edamam.com/search?q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}`
  );


Для создания динамики, нам нужно заменить это поле данными, которые пользователь будет вводить в поле input

Для сохранения данных ввода пользователя, нам потребуется еще две переменные "состояния", которые мы создадим использую, также как и ранее - useState

const [search, setSearch] = useState('');

Теперь нам нужно получить эти данные и записать их в эту переменную. Для этого создадим функцию


const updateSearch = e => {
  setSearch(e.target.value);
  console.log(search);
}



Как видно из кода выше, функция принимает объект события - e из которого данные значения этого события - e.target.value (а это и есть буква, введенная пользователем), мы записываем в переменную "состояния" - search, как метод хука состояния - search

setSearch(e.target.value)

В консоль выводим данную переменную, в которую поместили данные пользователя.

Теперь, подключим эту функцию к инпуту на событие изменения - onChange

onChange={updateSearch}

Посмотрим на то, что получилось в консоль



Там мы увидим все буквы введенные в инпут в соответствующем порядке.

Сейчас у нас есть приложение, которое сохраняет данные введенные пользователем в переменную, но если мы передадим эту переменную search непосредственно в тело запроса, на место chicken, как показано выше, то мы получим отправку запроса при каждой нажатии буквы. Это не самый правильный путь.

Отправка запроса только после его полного ввода.

Поэтому, мы создадим еще пару переменных "состояния", в которых и будет находиться наш запрос, который и будем передавать в поле запроса.

const [query, setQuery] = useState("chicken");

По умолчанию, там и будет та самая курица - "chicken".

Для того, чтобы полученные данные от пользователя из переменной search и передать их, по клику на кнопку формы, в переменную запроса - query, мы напишем такой код:

const getSearch = e => {
  e.preventDefault();
  setQuery(search);
}


Да, вот этим e.preventDefault(); уберем всплытие события и перезагрузку страницы, потому как функция у нас будет срабатывать на событие onSubmit самой формы:

<form onSubmit={getSearch} className="search-form">

Теперь в useEffect в качестве аргумента, передадим переменную - [query], как массив, за которой и будет следить реакт и при ее изменении делать запрос к серверу, то есть вызывать функцию получения данных:


useEffect(() => {
  getRecipes()
},[query])



В тело запроса, в функции getRecipes, вместе цыпленка - , мы передадим переменную запроса -


const getRecipes = async () => {
  const response = await fetch(
    `https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`
  );
  const data = await response.json()
  setRecipes(data.hits)
  console.log(data.hits)
}



Таким образом, мы сделали так, что все изменения, которые возникают в переменной, которая хранит состояние ввода пользователя, передаются в запрос, только тогда. когда пользователь нажимает на кнопку, или вводит Enter с клавиатуры.

App.js
import React, { useEffect, useState } from "react";
import Recipes from "./Recipes";
import "./App.css";

const App = () => {
  const APP_ID = "yourAPP-ID";
  const APP_KEY = "yourAPP-Key";
  const [recipes, setRecipes] = useState([]);
  const [search, setSearch] = useState("");
  const [query, setQuery] = useState("chicken");

  useEffect(() => {
    getRecipes();
  }, [query]);

  const getRecipes = async () => {
    const response = await fetch(
      `https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`
    );
    const data = await response.json();
    setRecipes(data.hits);
    console.log(data.hits);
  };

  const updateSearch = e => {
    setSearch(e.target.value);
    console.log(search);
  };

  const getSearch = e => {
    e.preventDefault();
    setQuery(search);
  };
  return (
    <div className="App">
      <form onSubmit={getSearch} className="search-form">
        <input
          className="search-bar"
          value={search}
          type="text"
          onChange={updateSearch}
        />
        <button className="search-button" type="submit">
          Search
        </button>
      </form>
      {recipes.map(recipe => (
        <Recipes
          key={recipe.recipe.label}
          title={recipe.recipe.label}
          calories={recipe.recipe.calories}
          image={recipe.recipe.image}
        />
      ))}
    </div>
  );
};

export default App;



В браузере мы сможем теперь спокойно отправлять запросы и получать нужные данные



На фото выше, видно, что данные с сервера получены, только после полного ввода и отправки запроса кнопкой.

Все изменения на этом этапе, вы можете посмотреть в моем репозитории на gitHub — react-recipe-edamam
ci -m "Searching on click button"


И меленькое дополнение. Для очистки поля ввода после нажатия кнопки, мы можем добавить в самый конец функции getSearch такую строку - setSearch('');.

Таким образом мы изменили переменную "состояния" search на пустую строку, сразу после отправки запроса, тем саммым мы очистили форму.

Добавим данные для вывода на страницу.

Если посмотреть на те данные, которые к нам приходят с сервера, в консоли, то мы можем увидеть, что там есть графа инградиенты, которую мы можем легко вывести для отображения на странице.



В файле App.js мы передадим эти данные, как пропсы для нашего компнента Recipes.js

        <Recipes
          key={recipe.recipe.label}
          title={recipe.recipe.label}
          calories={recipe.recipe.calories}
          image={recipe.recipe.image}
          ingredients={recipe.recipe.ingredients}
        />


В компоненте Recipes.js мы получим их деструктуризацией и выведем с помощью map.

import React from "react";

const Recipes = ({ title, calories, image, ingredients }) => {
  return (
    <div>
      <h1>{title}</h1>
      <p>{calories}</p>
      <ol>
        {ingredients.map((ingredient, index) => (
          <li key={index}>{ingredient.text}</li>
        ))}
      </ol>
      <img src={image} alt={title} />
    </div>
  );
};
export default Recipes;



Посмотрим в браузере:



Ингредиенты каждого компонента выводятся в виде списка.

Теперь нам нужно добавить немного стилей, для привлекательности нашего приложения.

Все изменения на этом этапе, вы можете посмотреть в моем репозитории на gitHub — react-recipe-edamam
ci -m "Output ingradients list and clean search input"


Стилизация приложения.

О различных способах стилизации реакт--приложений, я многократно рассказывал в своих постах по реакту, на этот раз я остановлюсь только лишь на модуле стиля. Стили вы можете написать сами, как вам будет угодно. Для этого вы можете использовать готовые библиотеки. Я же просто добавил для файла App.js такие стили:

Более подробно о стилях в Реакт я писал в посте - 5 способов написания стилей CSS в React.js

App.css
.App {
  min-height: 100vh;
  background-image: linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);
}

.search-form {
  min-height: 10vh;
  display: flex;
  justify-content: center;
  align-items: center;
}

.search-bar {
  width: 50%;
  border: none;
  padding: 10px;
}

.search-button {
  background-color: lightcoral;
  border: none;
  padding: 10px 20px;
  color: white;
}

.recipes {
  display: flex;
  justify-content: space-around;
  flex-wrap: wrap;
}


Для файла Recipes.js я создал отдельный файл recipes.module.css в котором написал следующее.

recipes.module.css
.recipe {
  border-radius: 10px;
  box-shadow: 0px 5px 20px rgb(71, 71, 71);
  margin: 20px;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  background-color: #fff;
  align-items: center;
  min-width: 40%;
}

.img {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  margin-bottom: 2%;
}


В файле Recipes.js я их подключил:


import React from "react";
 import style from "./recipe.module.css";

const Recipes = ({ title, calories, image, ingredients }) => {
  return (
    <div className={style.recipe}>
      <h1>{title}</h1>
      <p>{calories}</p>
      <ol>
        {ingredients.map((ingredient, index) => (
          <li key={index}>{ingredient.text}</li>
        ))}
      </ol>
      <img className={style.img} src={image} alt={title} />
    </div>
  );
};
export default Recipes;



Модули очень просты в использовании и приятны для многократного применения.

Надеюсь, что я вас не утомил подробностями. Буду рад вашим отзывам.

Это только основа приложения. Вы можете его изменить и дополнить, например - выводом на страницу отдельного рецепта, собственными стилями, и много чем еще.

Happy codding!

Все изменения на этом этапе, вы можете посмотреть в моем репозитории на gitHub — react-recipe-edamam
ci -m "Add styles and styles module"


Если интересно, то заходите в группу в facebook Facebook-Group, подписывайтесь на мой канал по ссылке ниже, или на мою страницу в facebook Yaroslav Web-Master

                                                                                                                                                             

Телеграм канал - Full Stack JavaScript Developer

Комментариев нет:

Отправить комментарий



Хотите освоить самые современные методы написания React приложений? Надоели простые проекты? Нужны курсы, книги, руководства, индивидуальные занятия по React и не только? Хотите стать разработчиком полного цикла, освоить стек MERN, или вы только начинаете свой путь в программировании, и не знаете с чего начать, то пишите через форму связи, подписывайтесь на мой канал в Телеге, вступайте в группу на Facebook.Пишите мне - kolesnikovy70 почта gmail.com