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

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

SVG line chart in React

Create SVG line chart in React

                                                                                                                                                             

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

четверг, 28 марта 2019 г.

Raect приложение с MLabDB ( II).





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

Давайте научим его делать это.

Внесение изменений в базу данных (checked)

Первое, что мы сделаем, это зададим в файле App.css класс, который будет добавляться к каждому таску, если у него checked: true и будет делать текст зачеркнутым.


 .completed {
    text-decoration: line-through;
}



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

TaskItem.js

import React, { Component } from "react";
import { Checkbox } from "muicss/react";

class TaskItem extends Component {
  constructor(props) {
    super(props)
    this.state = {
      task: props.task
    }
  }
  onChange(task, e) {
    this.props.onEditState(task, e.target.checked);
  }
  render() {
    return <div className="mui--divider-bottom">
      <Checkbox
        className={(this.state.task.completed) ? "completed" : ""}
        onChange={this.onChange.bind(this, this.state.task)}
        name={this.state.task._id.$oid}
        label={this.state.task.text}
        defaultChecked={this.state.task.completed}
      />
    </div>;
  }
}

export default TaskItem;



Так как у нас все взаимодействия с базой идут из файла App.js, то нам нужно оттуда и передавать данные в базу, поэтому поднимаемся на уровень выше - к родителю Tasks.js.

Здесь мы создадим функцию handleEditState(task, checked), которая принимает два параметра, а именно таск и отмеченный и передадим ее в компонент TaskItem.js, где мы ее написали внутри функции onChange.


import React, { Component } from 'react';
import { Panel } from 'muicss/react';
import TaskItem from './TaskItem';


class Tasks extends Component {
  handleEditState(task, checked) {
    this.props.onEditState(task, checked)
  }
  render() {
    let taskItems;
    if (this.props.tasks) {
      taskItems = this.props.tasks.map(task => {
        return (
          <TaskItem
            onEditState={this.handleEditState.bind(this)}
            key={task._id.$oid}
            task={task}
          />
        )
      })
    }
    return (
      <Panel>
        {taskItems}
      </Panel>
    );
  }
}

export default Tasks;




И теперь, наконец-то создадим самое основное в родителе - App.js

Вначале мы создадим саму функцию


editState(task,checked){
    console.log(task);
  }


теперь передадим ее компоненту:

<Tasks 
            onEditState={this.editState.bind(this)} 
            ... 



И если сейчас посмотреть в консоли приложения, то в момент, когда мы отмечаем чекбокс, мы видим в консоли все данные таска:



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



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

Теперь наша задача внести изменения в базу.

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

Обратите внимание, что в строку мы вставляем JS-код обычным сложением строк - конкатенацией.


"https://api.mlab.com/api/1/databases/react-tasks/collections/tasks/" + task._id.$oid + "?apiKey=yourAPIkey"



Метод запроса будет уже put. Передаем данные data. Создаем временную переменную let tasks , проходим циклом по всем таскам и если есть соответсвие по идентификатору, то меняем значение нужного таска completed на checked

И на основании этого изменяем состояние компонента: this.setState({ tasks: tasks });

Приведу код полностью


import React, { Component } from 'react';
import { Appbar, Container } from 'muicss/react';
import axios from 'axios';
import Tasks from './Components/Tasks';
import './App.css';

class App extends Component {
  constructor() {
    super()
    this.state = {
      tasks: []
    }
  }

  componentWillMount() {
    this.getTasks();
  }

  getTasks() {
    axios
      .request({
        method: "get",
        url:
          "https://api.mlab.com/api/1/databases/react-tasks/collections/tasks?apiKey=yourAPIkey"
      })
      .then(response => {
        this.setState(
          {
            tasks: response.data
          },
          () => {
            console.log(this.state);
          }
        );
      })
      .catch(error => {
        console.log(error);
      });
  }
  editState(task, checked) {
    axios
      .request({
        method: "put",
        url:
          "https://api.mlab.com/api/1/databases/react-tasks/collections/tasks/" + task._id.$oid + "?apiKey=yourAPIkey",
        data: {
          text: task.text,
          completed: checked
        }
      }).then((response) => {
        let tasks = this.state.tasks;
        for (let i = 0; i < tasks.length; i++) {
          if (tasks[i]._id.$oid === response.data._id.$oid) {
            tasks[i].completed = checked;
          }
        }
        this.setState({ tasks: tasks });
      }).catch(error => {
        console.log(error);
      });
  }
  render() {
    return (
      <div className="App">
        <Appbar>
          <Container>
            <table width="100%">
              <tbody>
                <tr>
                  <td className="mui--appbar-height">
                    <h3>React Tasks</h3>
                  </td>
                </tr>
              </tbody>
            </table>
          </Container>
        </Appbar>
        <br />
        <Container>
          <Tasks
            onEditState={this.editState.bind(this)}
            tasks={this.state.tasks}
          />
        </Container>
      </div>
    );
  }
}

export default App;



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

Ниже на фото вы можете увидеть, что все изменения чекбокса успешно попадают в базу.



Слева - браузер, справа - база данных MLab.

Таким образом мы получили сохранение изменений при отметке чекбокса в базу данных MLab (mongodb).

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

Например keys/apiKeys.js в переменной:

export const apiKey = 'ВАШ_КЛЮЧ_API';

и получать их в App.js импортом -

import {apiKey} from './keys/apikeys';

Это позволит легко добавить ключи в .gitignore и они не попадут, даже случайно, в "нечистые руки".

Код на данном этапе доступен по ссылке: react-mui-mlab-abc
ci -m "Transfer of changes to the database and their preservation."


Создание формы добавления задачи.

В папке компонентов для этого создадим отдельный файл AddTask.js Все содержимое приведу полностью, чтобы вы могли сразу понять, что в нем нет ничего необычного или достойного отдельного объяснения.


import React, { Component } from "react";
import { Form, Input } from "muicss/react";

class AddTask extends Component {
  constructor(props) {
    super(props)
    this.state = {
      task: ''
    }
  }
  onSubmit(e) {
    this.props.onAddTask(this.state.task);
    e.preventDefault();
  }

  onChange(e) {
    this.setState({ task: e.target.value })
  }
  render() {
    return <div className="mui--divider-bottom">
      <Form onSubmit={this.onSubmit.bind(this)}>
        <Input hint="Add Task" onChange={this.onChange.bind(this)} />
      </Form>
    </div>;
  }
}

export default AddTask;



Поля Form и Input из библиотеки MUI.

На поле ввода повесили событие на изменение его содержимого. В функции onChange сразу сохраняем изменения в стейт.

Кнопкой запустим функцию добавления задачи - onAddTask, куда и передадим значение стейта.

Функцию получим в props, поэтому мы ее создадим позже в родителе - App.js из которого и будем взаимодействовать с базой данных.

Теперь идем в файл App.js и импортируем в него вновь созданный компонент:

import AddTask from './Components/AddTask';

Выведем его на страницу сразу после Контейнера. Передадим ему как свойство функцию addTask и создадим ее сразу над render(). Пока что выведем в нее значение переменной text:


import React, { Component } from 'react';
import { Appbar, Container, Button } from 'muicss/react';
import axios from 'axios';
import Tasks from './Components/Tasks';
import AddTask from './Components/AddTask';
import './App.css';
import { apiKey } from './keys/apikeys';

class App extends Component {
  constructor() {
    super()
    this.state = {
      tasks: []
    }
  }

  componentWillMount() {
    this.getTasks();
  }

  getTasks() {
    axios
      .request({
        method: "get",
        url:
          "https://api.mlab.com/api/1/databases/react-tasks/collections/tasks?apiKey=" + apiKey
      })
      .then(response => {
        this.setState(
          {
            tasks: response.data
          },
          () => {
            console.log(this.state);
          }
        );
      })
      .catch(error => {
        console.log(error);
      });
  }
  editState(task, checked) {
    axios
      .request({
        method: "put",
        url:
          "https://api.mlab.com/api/1/databases/react-tasks/collections/tasks/" + task._id.$oid + "?apiKey=" + apiKey,
        data: {
          text: task.text,
          completed: checked
        }
      }).then((response) => {
        let tasks = this.state.tasks;
        for (let i = 0; i < tasks.length; i++) {
          if (tasks[i]._id.$oid === response.data._id.$oid) {
            tasks[i].completed = checked;
          }
        }
        this.setState({ tasks: tasks });
      }).catch(error => {
        console.log(error);
      });
  }

  addTask(text) {
       console.log(text);
  }

  render() {
    return (
      <div className="App">
        <Appbar>
          <Container>
            <table width="100%">
              <tbody>
                <tr>
                  <td className="mui--appbar-height">
                    <h3>React Tasks</h3>
                  </td>
                </tr>
              </tbody>
            </table>
          </Container>
        </Appbar>
        <br />
        <Container>
          <AddTask onAddTask={this.addTask.bind(this)} />
          <Tasks
            onEditState={this.editState.bind(this)}
            tasks={this.state.tasks}
          />
        </Container>
      </div>
    );
  }
}

export default App;



Если посмотреть на то, что получилось в браузере, то мы можем ввести какой-либо текст, нажать Enter и в консоли увидим это же значение:



Запрос на добавление данных в базу.

Все что нам нужно это написать функцию, которая будет взаимодействовать с базой при помощи axios

Пишем простой запрос. Указываем метод post, немного изменяем URL (это можно посмотреть в данных базы. Об этом рассказывал в первой части.).

Далее пишем данные, которые будем передавать.

Во временную переменную сохранили то что было в стейте до этого. Добавили методом push новые данные.

В данных completed: false, потому что мы предполагаем, что изначально задание не сделано.

При успешном выполнении вносим изменения в стейт.

В случае ошибки - поймали ее и вывели в консоль. Это если коротко...


  addTask(text) {
    axios
      .request({
        method: "post",
        url:
          "https://api.mlab.com/api/1/databases/react-tasks/collections/tasks?apiKey=" + apiKey,
        data: {
          text: text,
          completed: false
        }
      }).then((response) => {
        let tasks = this.state.tasks;
        tasks.push({
          _id: response.data._id,
          text: text,
          completed: false
        })
        this.setState({ tasks: tasks });
      }).catch(error => {
        console.log(error);
      });
  }



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



Вводим какой-либо текст. Нажимаем Enter и видим обновление данных на странице без перезагрузки и, соответственно данных в базе.

Для того, чтобы в этом убедиться, можно просто перезагрузить страницу и мы получим те же "новые" данные.

Код на данном этапе доступен по ссылке: react-mui-mlab-abc
ci -m "Added task to DataBase"


Добавление кнопки удаления выполненных заданий.

Начнем с тобой, что добавим саму кнопку в компонент.

В импорт из MUI добавим импорт кнопки:

import { Appbar, Container, Button } from 'muicss/react';

И прямо над закрывающим тегом контейнера, выведем саму кнопку удаления. Добавим ей красный цвет.

На клик повесим функцию, которую сейчас же и создадим.

        <Container>
          <AddTask onAddTask={this.addTask.bind(this)} />
          <Tasks
            onEditState={this.editState.bind(this)}
            tasks={this.state.tasks}
          />
          <Button color="danger" onClick={this.clearTasks.bind(this)} >
            Clear Tasks
          </Button>
        </Container>


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



Сделаем кнопку работающей.

Функция clearTasks

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

Для удобства, кол-во заданий вынесем в отдельную переменную - i

Пройдем по всем заданиям циклом while от большего к меньшему и если оно выполнено (completed), то методом splice уберем его из массива.

Здесь же внесем изменения в базу методом delete

Для работы с базой данных MongoDB нам нужен доступ не просто по идентификатору, а по _id.$oid

Ответ - стандартное подтверждение удаления и ошибку ловим в консоль.


  clearTasks() {
    let tasks = this.state.tasks;
    let i = tasks.length;

    while (i--) {
      if (tasks[i].completed === true) {
        let id = tasks[i]._id.$oid;
        tasks.splice(i, 1);
        axios.request({
          method: 'delete',
          url: "https://api.mlab.com/api/1/databases/react-tasks/collections/tasks/" + id + "?apiKey=" + apiKey,
        }).then(response => {

        }).catch((error) => {
          console.log(error)
        })
      }
    }
    this.setState({ tasks: tasks })
  }



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

Проверяем:



Код на данном этапе доступен по ссылке: react-mui-mlab-abc
ci -m "Delete completed tasks from DataBase"


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

К примеру, для очистки поля ввода можно сделать так.
  1. К самой форме ввода добавить id - компонент <Form id="form"/ >
  2. В функцию отправки onSubmit, добавить строку очистки всей формы: document.getElementById("form").reset();
  3.     onSubmit(e){
            this.props.onAddTask(this.state.task);
            e.preventDefault(); 
            document.getElementById("form").reset();  
        }
    
    


Данный проект в постах, можно рассматривать, скорее как учебный. Здесь все понятно. Я специально не пытался сокращать код, именно для его понимания и простоты.

Спасибо тем. кто дочитал до конца и

...Happy Coding!



                                                                                                                                                             

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

вторник, 26 марта 2019 г.

Raect приложение с MLabDB ( I ).

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



Приложение будет простое. Оно будет состоять из списка задач. Главное чем оно будет отличаться от обычного ToDo React-приложения, это возможностью сохранения данных после перезагрузки (открытия заново) нашего приложения.

Готовое приложение вы можете посмотреть на хостинге здесь.

Приступим.

Создаем базу данных на сервисе MLab

О регистрации и первых шагах по созданию базы данных, я многократно рассказывал в своих постах. Более подробно, вы можете посмотреть в посте - Блог на NodeJS Express MongoDB ( I ).

Если вы уже работали с этим сервисом, то тогда нажимаем кнопку "Create New" и создаем новую базу (документ).



Назовем ее (его), предположим, так - react-tasks.

Добавляем пользователя для БД, вводим пароль. Галочку - "Только чтение" не ставим!

Все данные сохраняем себе в блокнот.

Теперь создаем коллекцию - Add Collection



Далее нажимаем - Add Document



Данные в коллекцию добавим такие:


   {
    "text":"My Task One",
    "completed": false
   }



Обратите внимание, что значение boolean без кавычек. Фото ниже:



Вы можете добавить свои данные, в поля text и completed.

Нажимаем кнопку "Create and go back"



Таким же образом мы введем и вторую задачу.

Теперь мы имеем базу с двумя заданиями.



Создание приложения

Открываем КС (командую строку, если вы на винде, как и я) или другим способом в нужной директории создаем само Реакт-приложение. Для этого будем использовать готовое решение - create-react-app.

create-react-app reacttasks


reacttasks - название самого приложения.

Сразу после его установки, нам нужно перейти в него в КС и установить дополнительные модули:

cd reacttasks
npm install --save muicss axios


  1. muicss - для стилизации нашего приложения
  2. axios - HTTP-клиент для запросов.
После завершения утстановки, мы можем запустить наше приложение:

npm start


Обычное готовое приложение реакт запустится на порту 3000.

Удаляем лишнее

Удалим logo.svg, в файле App.css - всё содержимое, а в App.js все что внутри <div className="App">. Там мы что-нибудь напишем. например - My App. Это нужно только для того, чтобы убедиться сразу, что мы все сделали правильно. В браузере мы увидим эту надпись.

Добавляем Appbar

В самом верху, после импорта реакт, импортируем MUI :

import { Appbar, Container } from 'muicss/react';

Не забываем подключить и линк к public/index.html

<link href="//cdn.muicss.com/mui-0.9.41/css/mui.min.css" rel="stylesheet" type="text/css" media="screen" />

Его я взял с сайта MUI и от туда же возьмем готовый appbar . Уберем у него все лишнее и получим:



И следуя подсказки из консоли завернем все в контейнер:

import React, { Component } from 'react';
 import { Appbar, Container } from 'muicss/react';
import './App.css';

class App extends Component {

    render() {
        return (
            <div className="App">
                <Appbar>
                    <Container>
                        <table width="100%">
                            <tbody>
                                <tr>
                                    <td className="mui--appbar-height">
                                        <h3>React Tasks</h3>
                                    </td>
                                </tr>
                            </tbody>
                        </table>
                    </Container>
                </Appbar>
            </div>
        );
    }
}

export default App;





Ниже, внутри div, сделаем отступ и в контейнере напишем вывод нового компонента <Tasks /> и сразу его импортируем в наш файл App.js

import Tasks from './Components/Tasks';

App.js

import React, { Component } from 'react';
// Access all components from `muicss/react` module
import { Appbar, Container } from 'muicss/react';
import Tasks from './Components/Tasks';
import './App.css';

class App extends Component {

    render() {
        return (
            <div className="App">
                <Appbar>
                    <Container>
                        <table width="100%">
                            <tbody>
                                <tr>
                                    <td className="mui--appbar-height">
                                        <h3>React Tasks</h3>
                                    </td>
                                </tr>
                            </tbody>
                        </table>
                    </Container>
                </Appbar>
                <br />
                <Container>
                    <Tasks /&gr;
                </Container>
            </div>
        );
    }
}

export default App;



В папке src создадим папку Components, а в ней файл компонента - Tasks.js. В нем мы импортируем Panel из MUI, и выведем просто слово TASKS.

Tasks.js

import React, { Component } from 'react';
import { Panel } from 'muicss/react';

class Tasks extends Component {
    render() {
        return (
            
                TASKS
            
        );
    }
}

export default Tasks;




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





Код на данном этапе доступен по ссылке: react-mui-mlab-abc
ci -m "Create Navbar and Tasks Component"
(ci -m - коммит. В данном случае смотрите коммит - Retrieving data and outputting it in a component).

API MLab

В дальнейшем нам для работы потребуется получить доступ к API MLab по ключу.

Для этого, вам нужно выполнить следующие действия:

В правом верхнем углу аккаунта вы увидите нечто похожее:

{ user: "Имя Пользователя", account: "Псевдоним" }



нажимаем на поле user { user: "Имя Пользователя", account: "Псевдоним" }

попадем на страницу, где есть ваш ключ :



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


Ключ копируем и можно сохранить.

Более подробно можно прочитать на официальном сайте - API Authentication.

В целях безопасности храните ключ в тайне. Не давайте злоумышленникам получить доступ к вашим БД!


Получение данных от MLab по API

Сейчас наша задача состоит в том. чтобы получить данные, которые мы сохранили в базу на предварительном этапе. Это лучше всего сделать в компоненте жизненного цикла - componentWillMount()

Более подробно о жизненных циклах React-компонентов я писал в посте - React.js (6) Жизненный цикл компонентов.

Идем в файл App.js и создадим там функцию - getTasks(), которой мы будем обращаться к API MLab и получать данные.

Не забываем подключить axios.

Вызовем функцию поучения наших заданий в жизненном цикле компонента и сохраняем полученные данные в стейт.

Добавили вывод в консоль стейта.

App.js

import React, { Component } from 'react';
// Access all components from `muicss/react` module
import { Appbar, Container } from 'muicss/react';
import axios from 'axios';
import Tasks from './Components/Tasks';
import './App.css';

class App extends Component {

    constructor() {
        super()
        this.state = {
            tasks: []
        }
    }

    componentWillMount() {
        this.getTasks();
    }

    getTasks() {
        axios
            .request({
                method: "get",
                url:
                    "https://api.mlab.com/api/1/databases/react-tasks/collections/tasks?apiKey=yourAPIkey"
            })
            .then(response => {
                this.setState(
                    {
                        tasks: response.data
                    },
                    () => {
                        console.log(this.state);
                    }
                );
            })
            .catch(error => {
                console.log(error);
            });
    }
    render() {
        return (
            <div className="App">
                <Appbar>
                    <Container>
                        <table width="100%">
                            <tbody>
                                <tr>
                                    <td className="mui--appbar-height">
                                        <h3>React Tasks</h3>
                                    </td>
                                </tr>
                            </tbody>
                        </table>
                    </Container>
                </Appbar>
                <br />
                <Container>
                    <Tasks />
                </Container>
            </div>
        );
    }
}

export default App;


Более подробно о работе с API MLab

Смотрим в браузере. В консоли мы увидим, все наши таски:



Вывод данных на страницу.

Теперь мы можем передать полученные данные, как обычное состояние, дочернему компоненту. В данном случае - <Tasks tasks={this.state.tasks}/>



Для вывод каждого отдельного компонента (таска) мы создадим отдельный компонент - TaskItem .

import React, { Component } from "react";
import { Checkbox } from "muicss/react";

class TaskItem extends Component {
    constructor(props) {
        super(props)
        this.state = {
            task: props.task
        }
    }
    render() {
        return <div className="mui--divider-bottom">
                   <Checkbox name={this.state.task._id.$oid}
                        label={this.state.task.text} 
                        defaultChecked={this.state.task.completed}
                  />
               </div>;
    }
}

export default TaskItem;



Здесь ничего сложного. Мы передадим в этот компонент уже одну конкретную задачу из его родителя и выведем ее на экран в виде чекбокса с именем ( для этого используем идентификатор для каждого компонента - _id.$oid)



Родительскому div дадим класс с нижним подчеркиванием для красоты из MUI. В значение label выведем текстовое значение, а сделано\не сделано - как значение true \ false поля - completed.

Теперь нам нужно в теле самого компонента Tasks воспользоваться методом map() и передать в TaskItem одну задачу.

Для этого в методеrender компонента создадим пустую переменную - let taskItems;. Ниже проверим - Если пропсы пришли, то в эту переменную вернем наш компонент для каждого таска с данными. Эти данные для каждого отдельного компонента мы получаем методом map().

Tasks.js

import React, { Component } from "react";
import { Panel } from "muicss/react";
import TaskItem from "./TaskItem";

class Tasks extends Component {
  render() {
    let taskItems;
    if (this.props.tasks) {
      taskItems = this.props.tasks.map(task => {
        return <TaskItem key={task._id.$oid} task={task} />;
      });
    }
    return <Panel>{taskItems}</Panel>;
  }
}

export default Tasks;



Если посмотреть в браузере. то можно увидеть:



Теперь у нас есть те компоненты, которые мы создавали в БД на сервере MLab.

Давайте проделаем уже знакомые нам действия и добавим в базу третий таск с состоянием "completed": true



Добавляем.

И теперь у нас в базе три таска:



Перезагружаем браузер и...



Таким образом мы с вами получили доступ к БД MLab по API и вывели данные в React - приложение, стилизованное MUI.

Код на данном этапе доступен по ссылке: react-mui-mlab-abc
ci -m "Retrieving data and outputting it in a component"


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

                                                                                                                                                             

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


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