Translate

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

Показанные сообщения отсортированы по релевантности запросу "async/await". Сортировать по дате Показать все сообщения
Показанные сообщения отсортированы по релевантности запросу "async/await". Сортировать по дате Показать все сообщения

среда, 2 мая 2018 г.

ES6 для начинающих (2)


Первую часть этой статьи вы можете найти здесь.Там есть некоторые интересные функции. :)



Темы, которые я расскажу в этом посте
  1. Promises - Промисы (обещания)
  2. Async / Await


Promises

Promises - Обещания являются одной из полезных функций в ES6. Они используются для выполнения асинхронных операций, таких как запрос API, обработка файлов, загрузка изображений и т. д.

Итак, что такое Async? (держитесь, если вы уже знаете)

Асинхронные операции - это операции, требующие некоторого времени для завершения.

Например, скажем, вы определяете функцию, которая делает запрос API на сервер. Эта функция не возвращает результат немедленно. Для получения ответа от сервера требуется несколько секунд.

Поэтому, если вы вызываете эту функцию и присваиваете ее значение для некоторой переменной, она будет undefined не определено. Поскольку Javascript не знает, что функция обрабатывает некоторые операции async.

Итак, как мы справляемся с этим?

До этого поговорим немного о истории.

Перед обещаниями - promise программисты использовали обратные вызовы - callbacks. Обратные вызовы являются нормальными функциями в Javascript, который выполняется, когда операция async завершена.

Например, вы определяете функцию, которая делает запрос API на сервер. Затем вы указываете функцию обратного вызова, которая будет выполняться, когда мы получим ответ от сервера.

Таким образом, в приведенном выше случае Javascript не останавливает выполнение, пока мы не получим ответ от API. И мы определили функцию callback, которая будет выполнена после получения ответа. Думаю, это понятно.

Итак, что такое обещания - promises





Обещания promises- это объекты, которые помогают выполнять асинхронные операции.

Технически это объекты, которые представляют собой завершение операции async. (Если вы что-то не получите, то остаетесь на некоторое время.)

Прежде чем объяснять, как определить обещание promises, я объясню жизненный цикл обещания.

У нас есть три состояния в promises.
  1. Pending Ожидание: в этом состоянии обещание (promises) просто выполняет операцию async. Например, он делает некоторый запрос API на сервер или загружает некоторые изображения из cdn. Из этого состояния (promises) обещания можно перейти либо к Fulfilled - исполнению, либо к Rejected - отказу.
  2. Fulfilled : Если (promises) обещание достигло этого состояния, значит, операция async завершена, и у нас есть выходные данные - output. Например, у нас есть ответ от API.
  3. Rejected : Если (promises) обещание достигло этого состояния, это означает, что асинхронная операция не увенчалась успехом, и у нас есть ошибка, из-за которой операция завершилась неудачей.


Хорошо ... Давайте посмотрим на некий код.

const apiCall = new Promise(function(resolve, reject) {
 // async operation is defined here...
});



Promise определяется созданием конструктора с использованием ключевого слова - new. Тогда конструктор будет иметь функцию (мы называем ее executor function - функцией-исполнителем).

Асинхронная операция определяется внутри функции-исполнителя.

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

Первое решение параметра - фактически функция. Он вызывается внутри функции-исполнителя и представляет, что операция async успешна, и мы получаем результат. Функция Resolve помогает обещать перейти от pending -ожидаемого к fulfilled - исполненному состоянию. Надеюсь, вы поняли. :)

Подобно решению, reject также является функцией. Он также вызывается внутри функции-исполнителя, и он показывает, что операция async не выполняется, и мы получили ошибку. Отклонение помогает обещанию перейти от ожидания - pending к отклонению - reject. :)

const apiCall = new Promise(function(resolve, reject) {
 if ( API request to get some data ) {
  resolve("The request is successful and the response is "+ response);
 }
 else {
  reject("The request is not successful. The error is "+error);
 }
});



В приведенном выше коде вы можете видеть, что мы выполнили некоторую асинхронную операцию внутри функции-исполнителя. Затем вызывается функция resolve разрешения, если мы получаем ответ от сервера. И если есть некоторая ошибка, то функция reject error вызывается с сообщением об ошибке.

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

// calling the promise.
apiCall



Это оно. мы сделали. :) :)

Просто шучу. Это еще не конец.

В приведенном выше коде функция вызывается и выполняется обещание (т. Е. Выполняется executor function исполнительская функция). Затем вызывается функция resolve - разрешения или reject -отклонения на основе вывода - output.

Но вы можете видеть, что мы не обрабатывали результат, полученный из promise- обещания.

Например, если мы получим ответ от API, мы должны обработать ответ. Или, если мы получим ошибку, нам нужно правильно ее обработать.

Итак, как мы справляемся с этим?

Мы используем обработчики - handlers для получения результата от обещания.

Обработчики (handlers)- это просто функции, которые выполняются, когда происходит какое-то событие, например, нажатие кнопки, перемещение курсора и т. д.

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

Просто. :)

Давайте посмотрим, такой код:

// calling the promise with some handlers.
apiCall.then(function(x) {console.log(x); })



В приведенном выше коде мы приложили обработчик (handler) к (then) обещанию (promise). Затем - then обработчик получает параметр функции. Тогда сам параметр функции имеет параметр x.

Так что происходит.

Обработчик then выполняет function parameter, когда функция resolve вызывается внутри обещания (promise).

Я попытаюсь объяснить это снова.

Обработчик then ищет случай, когда вызывается функция разрешения. Поэтому, когда функция разрешенияresolve вызывается, then обработчик выполняет свой параметр функции.

apiCall.then(function(x) {console.log(x); })
// Output
The request is successful and the response is {name: "Jon Snow"}



Аналогично, есть еще один catch захват обработчика.

Обработчик catch ищет функцию отклонения - reject .

Функция catch выполняет свой функциональный параметр при вызове функции reject.

apiCall.then(function(x) {console.log(x); }).catch(function(x) {console.log(x); })
// Assuming the request is not successful ( reject function is called in the promise. )
Output:
The request is not successful



Думаю, вы поняли.

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

apiCall
.then(function(x) {
 console.log(x); 
})
.catch(function(x) {
 console.log(x);
}) 



Теперь это можно прочитать. Большинство программистов пишут это так.

Хорошо. Поэтому я думаю, что вы прошли долгий путь.

Давайте резюмировать..
  1. Promise определяется с помощью нового ключевого слова с параметром функции. Тогда сама функция имеет два параметра функции, которые разрешают - resolve и отклоняют - reject.
  2. Функция resolve будет вызвана, когда операция выполнена успешно.
  3. Функция reject - когда операция даст сбой.
  4. Обработчик then ищет функцию resolve разрешения.
  5. Обработчик catch ищет функцию reject отклонения.
  6. Удостоверьтесь в удобочитаемости кода :) :)
Вот рабочий пример - jsfiddle.net. Пожалуйста, попрактикуйтесь, если вы новичок в этом.


Надеюсь,что вы поймете пример. Тогда вперед.

Async / Await

Если вы понимаете Promise, то Async / Await довольно легко. И если вы не поняли Promise, Async / Await может помочь вам понять это. Может быть, вы также сможете получить ясный выход из Promise - обещаний. :)

Async

Ключевое слово Async позволяет любой функции возвращать только обещания.

Например, рассмотрите приведенный ниже код

async function hello() {
 return "Hello Promise..!"
}



Function hello вернет Promise - обещание.

Вышеприведенный код эквивалентен приведенному ниже коду.

function hello() {
 return new Promise(function(resolve, reject) {
 // executor function body.
 });
}



Так проще?

Другой пример:

async function hello(a, b) {
 if (a < b) {
  return "Greater";
 }
 else {
  return new Error("Not Greater");
 }
}
hello(14, 10)
.then(function(x) {
 console.log("Good..! " + x); 
})
.catch(function(x) {
 console.log("Oops..! " + x); 
})
Output:
Oops..! Not Greater. 
// if you call hello(4, 10) you get "Good..! Greater"



В приведенном выше коде мы определили функцию async и вернули некоторое значение или вернули некоторую ошибку.

Если вы возвращаете некоторое значение в async-функции, это эквивалентно вызову функции resolve разрешения.

Если вы возвращаете некоторую ошибку, вызывая error constructor using ‘new’ (конструктор ошибок с помощью «new»), то это эквивалентно функции reject - отклонения.

Не забывайте, что функция async вернет обещание. Поэтому, конечно, вы можете вызвать функцию resolve - разрешения и отклонения - reject внутри функции async.

Посмотрим, как это работает.

async function Max(a, b) {
 if (a > b) {
  return Promise.resolve("Success");
 }
 else {
  return Promise.reject("Error");
 }
}
Max(4, 10)
.then(function(x) {
 console.log("Good " + x); 
})
.catch(function(x) {
 console.log("Oops " + x); 
});
Output:
Oops Error
// If we pass Max(14, 10) then we should get "Good Success" :)



Await

Как следует из названия, это заставляет Javascript ждать завершения операции. Предположим, вы делаете запрос API с ключевым словом await. Это заставляет Javascript ждать, пока вы не получите ответ от конечной точки. И затем он возобновляет выполнение.

Хорошо .. Пойдем глубже

Ожидание await может использоваться только внутри асинхронной функции. Он не работает вне асинхронной функции
Давайте посмотрим пример:

async function hello() {
 let response = await fetch('https://api.github.com/');
 // above line fetches the response from the given API endpoint.
 return response;
}
hello()
.then(function(x) {
 console.log(x); 
});
...
...
Output:
Response from the API.



В приведенном выше коде вы можете видеть, что мы использовали await ожидание при получении ответа от API.

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

Обратите внимание, что ожидание операции останавливает выполнение внутри функции hello. Остающийся код вне функции hello не будет затронут. Выполнение продолжается вне функции. И когда мы получаем ответ, выполняется параметр функции .then внутри обработчика .

Надеюсь, вы поняли.

Давайте посмотрим пример

jsfiddle.net



В приведенном выше примере вы можете видеть, что мы использовали await функцию для getResponse. И getResponse вернет результат или ошибку через 5 секунд. Таким образом, до этого времени выполнение приостанавливается, а затем возвращается ответ.

Давайте посмотрим пример в реальном времени.

jsfiddle.net



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

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

Обработка ошибок очень проста в асинхронной async функции. Если ошибка возникает внутри функции async или когда ошибка возникает из других функций, которые вызываются внутри async, используя await, вызывается функция reject - отклонения. Просто.

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




Автор: Srebalaji Thirumalai.
Перевод: Kolesnikov Yaroslav




                                                                                                                                                             

четверг, 4 октября 2018 г.

Блог на NodeJS Express MongoDB ( II ).





Все материалы по ES6


Создание REST API роутов

В корне проекта создадим папку - routes.

Внутри нее создадим файл, который будет отвечать за роуты к постам - post.js

В файле сразу подключаем express - const express = require('express')

Далее создадим переменную, которая добывается из функции экспресс - Router()

const router = express.Router()

С этой переменной мы будем работать в этом файле и в конце экспортировать на ружу.

module.exports = router

Пока мы не пишем здесь никакого функционала. Давайте попробуем его применить для нашего сервера.

Для этого мы должны перейти в файл app.js, где мы описываем сервер и подключит данный роутер.

const postRouter = require('./routes/post.js').


const express = require('express')
const mongoose = require('mongoose')
const path = require('path')
const postRouter = require('./routes/post')
const keys = require('./keys')

const port = process.env.PORT || 5000
const clientPath = path.join(__dirname, 'client')

mongoose.connect(keys.mongoURI)
			.then(() => console.log('MongoDB connected'))
			.catch(err => console.error(err))	
const app = express()
app.use(express.static(clientPath))
app.listen(port,()=>{
	console.log(`Server run on ${port} port`)
})



Теперь, для того чтобы использовать наш роутер, мы должны, после того, как определили объект нашего приложения =>const app = express() (ниже) написать команду

app.use('', postRouter)

Для того чтобы применить какой то роут и экспресc понимал ссылки.

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

Вторым- тот роутер, который нам нужно использовать.

Теперь нужно подумать какой префикс нам здесь написать?

По сути, у нас будет часть сервера, которая будет отдавать клиентскую сторону - index.html и у нас будет часть сервера, которая будет отвечать за REST API. Соответственно, все что будет делаться через REST API мы можем начинать с префикса /api/ и далее, так как у нас идет здесь идет работа именно с постами, мы можем указывать сущьности - например post

app.use('/api/post', postRouter)

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

Перейдем в файл post.js

По сути для нашего приложения важно реализовать три действия:
  1. - получит все посты, которые есть в БД
  2. - создать новый пост
  3. - реализовать удаление какого-то поста
В REST API для того, чтобы получать что-то мы используем http-запросы с методом get.

Для того, чтобы реализовать метод get в экспрессе мы обращаемся к роутеру (router) и вызываем у него метод get.

Здесь в качестве параметра мы просто указываем слэш - '/'.

Вторым параметром указываем callback-функцию, которая принимает два параметра ( req -request - запрос res - response - ответ.)

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


router.get('', (req, res) => {
	
})



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

У нас есть некоторый префикс - '/api/post' и это означает, что мы будем видеть запрос следующего характера:

http://localhost:5000/api/post

который будет реагировать на тип GET. И в случае, если экспресс увидит, что это запрос GET, то будет нам что-то отдавать.

Похожая ситуация у нас будет реализована еще для двух методов.

В REST API если мы хотим что-то создать, то мы используем метод POST, удалить - DELETE.

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

routes/post.js


const express = require('express')

const router = express.Router()
// http://localhost:5000/api/post GET
router.get('', (req, res) => {

})
// http://localhost:5000/api/post POST
router.post('', (req, res) => {
	
})
// http://localhost:5000/api/post DELETE
router.delete('', (req, res) => {
	
})

module.exports = router



Сейчас, давайте попробуем описать метод GET с помощью которого мы хотим что-то получить.

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

const Post = require('../models/Post')

Теперь с помощью этого объекта мы уже можем работать с БД.

Для того, чтобы получить все сущьности из БД, нам нужно в методе GETобратиться к и вызвать у него метод find(), передав в метод пустой объект. Это означает, что мы будем требовать все посты без каких либо условий.


router.get('', (req, res) => {
	Post.find({})
})



Данная операция является асинхронной. Она нам будет возвращать некий промис мы его можем ченить с помощью метода .then и после этого как-то обрабатывать.


router.get('', (req, res) => {
	Post.find({}).then((posts)=>{
 	//что-то делать...	
 })
})



Мы пойдем более современным путем. Так как мы работает на платформе NodeJS, которая не требует понимания разных браузеров, то здесь можно использовать все новшества современного JS ES6.

Поэтому мы смело можем использовать синтаксис - async/await.

Вместо того, чтоб так ченить промисы.

Мы можем сказать NodeJS, что данная функция будет являться асинхронной и поставить вначале нее async

И теперь, чтобы получить список этих постов, мы можем просто создать переменную posts и с помощью префикса await - подождать.

const posts = await Post.find({})

Эта конструкция полностью идентична

	Post.find({}).then((posts)=>{
          // ...
	})



При этом мы написали полностью асинхронный код и у нас нет вложенных функций.

Таким образом мы получили:


const express = require('express')

const router = express.Router()

const Post = require('../models/Post')
// http://localhost:5000/api/post GET
router.get('', async(req, res) => {
const posts = await Post.find({})
})
// http://localhost:5000/api/post POST
router.post('', (req, res) => {
	
})
// http://localhost:5000/api/post DELETE
router.delete('', (req, res) => {
	
})

module.exports = router



Думаем над тем. что нам делать еще в методе GET По сути, мы должны что-то отдать клиенту (список постов). Для того, чтобы ответить клиенту мы можем воспользоваться объектом res, затем указать статус

res.status(200) и с помощью метода json указать те данные, которые мы хотим отправить обратно - posts.

res.status(200).json(posts)


const express = require('express')

const router = express.Router()

const Post = require('../models/Post')
// http://localhost:5000/api/post GET
router.get('', async(req, res) => {
	const posts = await Post.find({})
	res.status(200).json(posts)
})
// http://localhost:5000/api/post POST
router.post('', (req, res) => {
	
})
// http://localhost:5000/api/post DELETE
router.delete('', (req, res) => {
	
})

module.exports = router



Коротко: мы просто ждем, что сто-то придет к нам с базы данных и после этого отправляем их клиенту.

Метод POST

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

Создадим переменную postData и это будет объект с полями title и text. Здесь мы будем с клиента отправлять данные в БД. Для сбора данных и нужны эти поля.

Теперь эти данные нужно обработать. Это сделать просто. У нас есть объект req с помощью которого будет сделан запрос и сказать ему, что например, title хранится в req.body.title.


router.post('', (req, res) => {
	const postData = {
		title: req.body.title,
		text: req.body.text,
	}
})



Теперь у нас есть данные необходимые для создания нового поста в базе и мы можем создать новый пост с помощью модели Post на основе данных postData.

const post = new Post(postData)

И после этого нам нужно сохранить это все в БД. Для этого можно просто написать:

await post.save()

И сделать функцию асинхронной:


router.post('', async (req, res) => {
	const postData = {
		title: req.body.title,
		text: req.body.text,
	}
	const post = new Post(postData)
	await post.save()
})



Этой строкой -

await post.save()

мы будем ждать, когда код сохранится на сервере. Далее объект post изменится, потому что придет ответ с БД и у нас будет уникальный ID, который нам выдаст MongoDB и у нас определится дата. когда был создан пост..

И теперь мы можем ответить (res)клиенту оперируя теми данными, которые мы получили с БД.

В RESTAPI если мы что-то создаем, то используем статус 201, а далее методом json мы указываем. чо будем обратно возвращать post.

res.status(201).json(post)


// http://localhost:5000/api/post POST
router.post('', async (req, res) => {
	const postData = {
		title: req.body.title,
		text: req.body.text,
	}
	const post = new Post(postData)
	await post.save()
	res.status(201).json(post)
})



Метод DELETE

Он будет тоже асинхронным, как и предыдущие методы. (Добавим async).

В этом методе нам нужно будет удалить пост по ID. Поэтому здесь уже не подойдет такой url - http://localhost:5000/api/post, потому что нам в нем надо передать id того поста, который мы хотим удалить. Например id =34;

http://localhost:5000/api/post/34

Эта часть будет динамическая.

Для того, чтобы указать какую-либо динамическую часть в Экспрессе, нам достаточно поставить после слэша двоеточие и написать id.

Именно так - id будет называться параметр, который мы будем получать в этом месте.

Теперь нам нужно с помощью модели Post вызвать метод remove(), который позволяет нам что-то удалять из базы. и далее мы говорим условия - какой именно пост мы хотим удалить в объекте. Мы хотим удалить пост у которого _id === id который придет в роуте. А этот параметр хронися в объекте запроса req (req.params.is)

Так как функция асинхронная, то мы ее должны подождать - await


router.delete('/:id', async (req, res) => {
	await Post.remove({_id: req.params.id})
})



Как только удалится нужный нам пост мы можем ответить клиенту статусом 200 и в json отправить мессендж -"Удалено"


router.delete('/:id', async (req, res) => {
	await Post.remove({_id: req.params.id})
	 res.status(200).json({
	 	message: 'Удалено'
	 })
})



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

Файл post.js


const express = require('express')

const router = express.Router()

const Post = require('../models/Post')
// http://localhost:5000/api/post GET
router.get('/', async (req, res) => {
	const posts = await Post.find({})
	res.status(200).json(posts)
})
// http://localhost:5000/api/post POST
router.post('/', async (req, res) => {
	const postData = {
		title: req.body.title,
		text: req.body.text,
	}
	const post = new Post(postData)
	await post.save()
	res.status(201).json(post)
})
// http://localhost:5000/api/post DELETE
router.delete('/:id', async (req, res) => {
	await Post.remove({_id: req.params.id})
	 res.status(200).json({
	 	message: 'Удалено'
	 })
})

module.exports = router



Далее перейдем к тестированию, создав некую клиентскую часть.

Тестирование серверной части.

Всё, что относится к фронтенд части хранится в папке client. Там, пока что, один файл - index.html

Начнем с верстки. На ней я подробно останавливаться не буду. Отмечу только, что для адаптивности и простоты я использовал фреймворк Materialize.

Подключаем css, material icons и скрипт js к нашему index.html файлу через CDN (ссылки на сайте) . После скрипта материалайза подключаем наш скрипт index.js, который мы создадим в той же папке.

В нем мы будем вести разработку на нативном JS.

В файле index.html создадим div - контейнер, которому зададим стиль, чтобы он не прилипал к верху экрана и в него мы будем выводить посты.


<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
	<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
	 <!-- Compiled and minified CSS -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">	
  <!--Import Google Icon Font-->
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
	<title>FS-Blog</title>
   <!--[if IE]>
   <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
 <![endif]-->
</head>
<body>
	<div class="container center" style="padding-top: 50px;">
		<div class="row">
			<div class="col s12 m8 offset-m2 lg6 offset-3" id="post">
				<div class="card">
	        <div class="card-content">
	          <span class="card-title">Post Title</span>
	          <p>I am a very simple card. I am good at containing small bits of information.</p>
	          <small>12.12.1212</small>
	        </div>
	        <div class="card-action">
						<button class="btn btn-small red">
							<i class="material-icons">Delete</i>
						</button>	
	        </div>
	      </div>
			</div>
		</div>
	</div>


	<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
	<script src="index.js"></script>
</body>
</html>



Запускаем сервер

npm run dev


Переходим на сайт localhost:5000 и видим:



Теперь перейдем к клиентскому скрипту и посмотрим как вывести список всех постов.

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


<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
	<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
	 <!-- Compiled and minified CSS -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">	
  <!--Import Google Icon Font-->
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
	<title>FS-Blog</title>
   <!--[if IE]>
   <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
 <![endif]-->
</head>
<body>
	<div class="container center" style="padding-top: 50px;">
		<div class="row">
			<div class="col s12 m8 offset-m2 lg6 offset-3" id="post">
		<div class="card z-depth-4">
	        <div class="card-content">
	          <span class="card-title">Post Title</span>
	          <p>I am a very simple card. I am good at containing small bits of information.</p>
	          <small>12.12.1212</small>
	        </div>
	        <div class="card-action">
						<button class="btn btn-small red">
							<i class="material-icons">Delete</i>
						</button>	
	        </div>
	      </div>
			</div>
		</div>
	</div>


	<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
	<script src="index.js"></script>
</body>
</html>



И так как здесь у нас нативный JS, то мы создадим подобие компонента в переменной card


const card = post => {
	return `
		<div class="card z-depth-4">
	           <div class="card-content">
	            <span class="card-title">${post.title}</span>
	            <p>${post.text}</p>
	            <small>${post.date}</small>
	           </div>
	          <div class="card-action">
		    <button class="btn btn-small red">
			<i class="material-icons">Delete</i>
		    </button>	
	        </div>
	      </div>
	`
}



Функция, которая принимает post и отдает html разметку. Данные в которой мы выводим как переменные.

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

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

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

Допустим, что это будет класс PostApi. У него будет несколько методов.

Первый статический static fetch(), в который мы не будем передавать параметры. Этим методом мы будем делать запрос на получение всех наших постов.

В JS есть нативная функция fetch(), с помощью нее мы как раз и можем делать запросы.

Первым параметром в эту функцию нам надо передать url по которому мы хотим сделать запрос.

Для этого мы создадим новую константу - const BASE_URL='api/post' - Это та самая строчка. которая обозначена в файле app.js.

Вторым параметром мы указываем метод с помощью которого мы хотим сделать запрос. В нашем случае, когда мы хотим получить список всех потов это метод GET. ({method: get}.

Далее, метод fetch() возвращает промис и в методе .then мы можем привести тот формат который прилетает к нам с сервера и обрабатывается JS, привести в понятный формат объектов. Поэтому - res=>res.json() и все это вернем из метода fetch().


const BASE_URL = '/api/post'

clss PostApi {
	static fetch(){
		return fetch(BASE_URL, {method: get}).then(res=>res.json())
	}
}



И теперь в какой момент нам необходимо сделать запрос?

При загрузке страницы.

На документ поставим обработчик события.

document.addEventListener('DOMContentLoaded', () => {
	
})


Как только запускаем приложение нам нужно сделать запрос к бекенду, поэтому мы обращаемся к PostApi.fetch и вызываем метод .fetch() и так как он возвращает промис, то .then(). В этом методу мы будем получать объект backendPosts. То есть это будет уже тот массив, который прилетит от сервера. И теперь, нам нужно просто вывести все эти посты в html.

Для этого создадим наверху файла переменную let posts = []. Именно в ней мы будем хранить все посты, которые у нас есть.

Когда мы получим наши посты то в эту переменную мы положим содержимое backendPosts на котором вызовем метод concat, для предотвращения проблем мутаций.

Он просто сгенерирует дубликат данного массива.


const BASE_URL = '/api/post'
let posts =[]
clss PostApi {
	static fetch(){
		return fetch(BASE_URL, {method: 'get'}).then(res=>res.json())
	}
}

document.addEventListener('DOMContentLoaded', () => {
	PostApi.fetch().then(backendPosts => {
		posts = backendPosts.concat()
	})
})



Теперь, когда у нас есть список всех постов с бэкенда и нам остается их отрендерить.

Для этого создадим функцию renderPosts(posts) куда и передадим эти посты.

Функцию опишем ниже. По умолчанию она будет принимать пустой массив - _posts=[]

У нас есть id="posts" в файле index.html куда нам нужно выводить наши посты.

$ - просто обозначает, что это некоторый элемент.


function renderPosts(_posts=[]) {
	const $posts = document.querySelector('#posts')

}



Далее, проверим. Если длина массива пришедшего в функция больше 0, то есть у нас есть некоторые посты, то мы будем генерировать карточки. Иначе мы просто выведем сообщение "Постов пока нет".

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

Мы можем обратиться к локальной переменной _posts и далее с помощью метода .map() мы на каждой итерации будем получать определенный post (это объект) и на выходе вернуть html, то есть вызвать функцию card(post)

Преобразуем массив к строке методом .join(' '). Через пробел.


function renderPosts(_posts=[]) {
	const $posts = document.querySelector('#posts')
	if(_posts.length > 0) {
		$posts.innerHTML = _posts.map(post=> card(post)).join()	 
	} else {
		$posts.innerHTML = `
Постов пока нет.
` } }


Сохраняем и обновляем страницу Идем на вкладку Networkв в консоли.



Видим, что "Постов пока нет", а в запросах - url совпадает и метод GET, и получаем пустой массив post



С выводом постов мы разобрались.

Единственное что мы можем добавить, это индикатор загрузки.

Имитируем задержку сервера функцией setTimeout пред рендерингом постов, а на это время сделаем показ индикатора загрузки.


document.addEventListener('DOMContentLoaded', () => {
	PostApi.fetch().then(backendPosts => {
		posts = backendPosts.concat()
		setTimeout(()=>{
		renderPosts(posts)
		}, 2000)			
	})
})



Прелоудер возьмем на сайте materializecss.com

Скопируем там готовый код прелоудера и добавим его в наш див поста в файле index.html

Файл index.html полностью:


<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
	<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
	 <!-- Compiled and minified CSS -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">	
  <!--Import Google Icon Font-->
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
	<title>FS-Blog</title>
   <!--[if IE]>
   <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
 <![endif]-->
</head>
<body>
	<div class="container center" style="padding-top: 50px;">
		<div class="row">
			<div class="col s12 m8 offset-m2 lg6 offset-3" id="posts">
				<div class="preloader-wrapper small active">
					<div class="spinner-layer spinner-green-only">
						<div class="circle-clipper left">
							<div class="circle"></div>
						</div><div class="gap-patch">
							<div class="circle"></div>
						</div><div class="circle-clipper right">
							<div class="circle"></div>
						</div>
					</div>
				</div>
			</div>
		</div>
	</div>


	<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
	<script src="index.js"></script>
</body>
</html>



Файл index.js полностью:



const card = post => { return ` <div class="card z-depth-4"> <div class="card-content"> <span class="card-title">${post.title}</span> <p>${post.text}</p> <small>${post.date}</small> </div> <div class="card-action"> <button class="btn btn-small red"> <i class="material-icons">Delete</i> </button> </div> </div> ` } const BASE_URL = '/api/post' let posts =[] class PostApi { static fetch(){ return fetch(BASE_URL, {method: 'get'}).then(res=>res.json()) } } document.addEventListener('DOMContentLoaded', () => { PostApi.fetch().then(backendPosts => { posts = backendPosts.concat() setTimeout(()=>{ renderPosts(posts) }, 2000) }) }) function renderPosts(_posts=[]) { const $posts = document.querySelector('#posts') if(_posts.length > 0) { $posts.innerHTML = _posts.map(post=> card(post)).join() } else { $posts.innerHTML = `<div class="center">Постов пока нет.</div>` } }


Теперь перед загрузкой постов мы будем видеть индикатор - вращающиеся полуокружность и через 2 сек. сообщение "Постов пока нет".





                                                                                                                                                             

пятница, 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

понедельник, 11 февраля 2019 г.

React - Интерактивная таблица. (I)

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



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

Готовое приложение здесь

Приложение будем создавать с помощью готового решения от разработчиков Facebook - create-react-app.

Я не буду на этом останавливаться, потому что много раз уже рассказывал об этом в постах о React. О самом приложении, вы сможете подробно почитать на странице github - /facebook/create-react-app

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

В папке src я оставил только три файла: App.js, index.js и serviceWorker.js

В файле App.js я удалил все ненужные подключения (импорты) и внутренность компонента App, а так как мы будем использовать Bootstrap для отображения наших компонентов, то я сразу добавил компоненту класс Bootstrap - className="container"


import React, { Component } from 'react';


class App extends Component {

  render() {
    return (
      <div className="container">
          MyApp
      </div>
    );
  }
}

export default App;



Устанавливаем и подключаем Bootstrap

Для простоты реализации наших задач с таблицей, мы будем использовать библиотеку Bootstrap. Для ее установки нам необходимо, находиться в папке нашего проекта (Если вы используете Visual Studio Code, то просто - создать терминал, а если КС - командную строку Windows или терминал, то перейти в папку проекта и набрать:

npm install bootstrap


или, если вы используете yarn

yarn add bootstrap


Я буду использовать npm, поэтому здесь и далее команды для npm.

После того, как закончится установка, в файле package.json, в разделе зависимостей - dependencies вы увидите установленные пакеты "bootstrap": "^4.2.1"



Теперь нам нужно его подключить в файле index.js


import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
import 'bootstrap/dist/css/bootstrap.min.css'

ReactDOM.render(, 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: http://bit.ly/CRA-PWA
serviceWorker.unregister();



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

В консоли набираем:

npm start


Сервер запустится и наше приложение запустится на порту 3000.

Для остановки сервера, вы можете использовать сочетание клавиш Ctrl + C



На фото выше, вы можете видеть, что сервер запустился, приложение работает и Bootstrap подключен ( на контейнер действуют стили самого Bootstrap - отмечено красным).

На этом этапе код по ссылке: react-table-abc
ci-m (здесь и далее - коммит) "deleted unnecessary files and import bootstrap"

Получаем данные.

Данные для таблицы мы будем получать из сети по этой ссылке. http://www.filltext.com/?rows=32&id={number|1000}&firstName={firstName}&lastName={lastName}&email={email}&phone={phone|(xxx)xxx-xx-xx}&address={addressObject}&description={lorem|32}

Если перейти по ней, то можно увидеть все данные, которые будут переданы нам в формате json



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

Очень важно здесь понять, что нам нужно получить данные из сети тогда, когда у нас будет уже сформировано DOM дерево.Именно поэтому мы будем использовать жизненный цикл React компонента, а именно - componentDidMount

Написание кода в новом формате, позволяет нам использовать новые возможности, а именно асинхронные запросы- asinc/await. Если вы хотите вспомнить детали, то посмотрите в моем блоге

Для получения данных, мы используем метод fetch() в который передадим эту ссылку внутри обратных кавычек.

Вся конструкция в компоненте App будет выглядеть так:


import React, { Component } from 'react';
import Loader from './Loader/Loader';
import Table from './Table/Table';

class App extends Component {

  async componentDidMount() {
    const response = await fetch(` http://www.filltext.com/?rows=32&id={number|1000}&firstName={firstName}&lastName={lastName}&email={email}&phone={phone|(xxx)xxx-xx-xx}&address={addressObject}&description={lorem|32}`)
    const data = await response.json()
     console.log(data)
  }
  render() {
    return (
      <div className="container">
         MyApp
      </div>
    );
  }
}

export default App;




Таким образом мы вернем в объект response промис. А в объект data положим результат работы response и его метода - JSON.

Для отображения объекта data мы его выведем в консоль.

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



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



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

На этом этапе код по ссылке: react-table-abc
ci -m "Asinc request to the date for table"

Индикатор загрузки.

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

Для решения этой задачи, мы не будем выдумывать свой велосипед и возьмем готовое решение с сайта Pure CSS Loaders.

В папке src мы создадим отдельную, одноименную папку для компонента Loader и поместим туда два файла: Loader.js и Loader.css.

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

Как вы догадались, css-код мы отправляем в файл Loader.css практически без изменений. Я только изменил цвет индикатора на синий и добавил ему позиционирование по центру страницы:


.lds-dual-ring {
    width: 64px;
    height: 64px;
    position: absolute;
    top: 50%;
    left: 50%;
  }
  .lds-dual-ring:after {
    content: " ";
    display: block;
    width: 46px;
    height: 46px;
    margin: 1px;
    border-radius: 50%;
    border: 5px solid rgb(40, 3, 253);
    border-color: rgb(40, 3, 253) transparent rgb(40, 3, 253) transparent;
    animation: lds-dual-ring 1.2s linear infinite;
  }
  @keyframes lds-dual-ring {
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
  }



В файле Loader.js мы импортируем сам React для создания компонента и стили.

Далее, создадим "глупый" простой компонент, как безымянную функцию и сразу же ее экспортируем. Функция будет возвращать пустой div элемент, которому мы дадим класс, который взяли из html - кода нашего индикатора загрузки:


import React from 'react';
import './Loader.css';

export default () => <div className="lds-dual-ring" />



При написании компонентов, стоит помнить, что задавать их через классы и делать их "умными" нужно только тогда, когда компонент будет хранить какую-то логику действий. А если он только отображает какой-то элемент на странице, то лучше всего написать его простой функцией.


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

Сейчас нам нужно импортировать компонент Loader в компонент App и вывести его на страницу, вместо текста "MyApp".

Это очень простая задача и я не буду на ней останавливаться. Если не получится сделать самостоятельно, то я приведу код App компонента полностью ниже.



На этом этапе код по ссылке: react-table-abc
ci -m "Loader"

Формируем таблицу.

Для таблицы нам потребуется компонент Table для которого мы создадим отдельную папку в папке src и в ней файл Table.js.

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

В него мы будем выводить данные в виде таблицы. Для этого мы будем использовать готовую таблицу, которую мы возьмем с сайта Bootstrap Bootstrap Tables- отсюда полностью скопируем thead таблицы. В коде ниже - отметил синим.

В компоненте мы будем возвращать саму таблицу table className="table", внутри поместим thead и ниже него создадим tbody, куда мы и будем выводить нами полученные данные из переменной data, которые мы передадим в виде props нашему компоненту Table в компоненте App.

Если я, все-таки вас запутал, то не огорчайтесь, начнем все по порядку.

Представим, что наш компонент Table уже получил эти данные в виде props с именем data. Наша задача вывести их по отдельности в каждую ячейку таблицы. Для этого мы используем метод map() В коде ниже отметил красным.


import React from 'react';

export default props => (
    <table className="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>First Name</th>
                <th>Last Name</th>
                <th>E-mail</th>
                <th>Phone</th>
            </tr>
        </thead>
        <tbody>
            { props.data.map(item =>(
                <tr key={item.id}>
                    <td>{item.id}</td>
                    <td>{item.firstName}</td>
                    <td>{item.lastName}</td>
                    <td>{item.email}</td>
                    <td>{item.phone}</td>
                </tr>
            ))}
        </tbody>
    </table>
)



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

Теперь, переходим в компонент App и там импортируем нашу таблицу - компонент Table

import Table from './Table/Table';

Сейчас нам нужно сделать так, чтобы пока идет загрузка, показывать компонент Loader, как только загрузка закончилась, то Table

Для этого мы в компоненте App создадим состояние - state, в котором создадим поле isLoading: со значением true и поле data, которое равно пустому массиву.


import React, { Component } from 'react';
import Loader from './Loader/Loader';
import Table from './Table/Table';

class App extends Component {
  state ={
    isLoading: true,
    data: [],
  }
  async componentDidMount() {
    const response = await fetch(` http://www.filltext.com/?rows=32&id={number|1000}&firstName={firstName}&lastName={lastName}&email={email}&phone={phone|(xxx)xxx-xx-xx}&address={addressObject}&description={lorem|32}`)
    const data = await response.json()
   console.log(data)
  }
  render() {
    return (
      <div className="container">
      {
        this.state.isLoading 
        ? <Loader />
        : <Table 
        data={this.state.data}
        />
      }
      </div>
    );
  }
}

export default App;



В методе render мы проверим. Если this.state.isLoading , то мы показываем компонент Loader, а если данные уже загрузили, то показываем таблицу.

В сам компонент Table мы передадим данные, о которых говорили выше - data={this.state.data}

Теперь нам нужно изменить state, как только загрузятся данные.

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


  async componentDidMount() {
    const response = await fetch(` http://www.filltext.com/?rows=32&id={number|1000}&firstName={firstName}&lastName={lastName}&email={email}&phone={phone|(xxx)xxx-xx-xx}&address={addressObject}&description={lorem|32}`)
    const data = await response.json()
    // console.log(data)
    this.setState({
      isLoading: false,
      data
    })

  }



В функции изменения состояния мы имеем право просто написать data, потому что у нас совпадают ключ и значение (data: data).

Все вместе:


import React, { Component } from 'react';
import Loader from './Loader/Loader';
import Table from './Table/Table';

class App extends Component {
  state ={
    isLoading: true,
    data: [],
  }
  async componentDidMount() {
    const response = await fetch(` http://www.filltext.com/?rows=32&id={number|1000}&firstName={firstName}&lastName={lastName}&email={email}&phone={phone|(xxx)xxx-xx-xx}&address={addressObject}&description={lorem|32}`)
    const data = await response.json()
    // console.log(data)
    this.setState({
      isLoading: false,
      data
    })

  }
  render() {
    return (
      <div className="container">
      {
        this.state.isLoading 
        ? <Loader />
        : <Table 
        data={this.state.data}
        />
      }
      </div>
    );
  }
}

export default App;



Посмотрим на приложение:



Мы увидим предупреждение. Это значит, что нам передали два элемента с одинаковым id, поэтому нам нужно внести некоторые изменения в наш ключ - key в файле Table.js и сделать его равным не просто id, а прибавить к нему, например телефонный номер для большей уникальности:

<tr key={item.id + item.phone}>

Теперь у нас есть таблица с данными.

На этом этапе код по ссылке: react-table-abc
ci -m "create table"

Сортировка элементов в таблице.

Наша задача сделать так, чтобы по клику на заголовок таблицы, данные в каждом столбце сортировались в порядке возрастания (чисел и по алфавиту). При повторном клике, сортировка в обратную сторону.

Для этого нам нужно написать функцию сортировки (мы это сделаем с помощью библиотеки Lodash), передать ее в виде props компоненту Table, повесить событие onClickс этой функцией на заголовок таблицы и передать в эту функцию название столбца ( id, firstName, lastName или phone ).

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

onClick={props.onSort.bind(null, 'id')}

Table.js

<tr>
  <th onClick={props.onSort.bind(null, 'id')}>ID</th>
  <th onClick={props.onSort.bind(null, 'firstName')}>First Name</th>
  <th onClick={props.onSort.bind(null, 'lastName')}>Last Name</th>
  <th onClick={props.onSort.bind(null, 'email')}>E-mail</th>
  <th onClick={props.onSort.bind(null, 'phone')}>Phone</th>
</tr>



Теперь, нам нужно передать эту функцию в виде свойства компоненту <Table /> : onSort={this.onSort}


{
  this.state.isLoading 
  ? <Loader />
  : <Table 
  data={this.state.data}
  onSort={this.onSort}
  />
}



Сразу над методом render(), компонента App мы напишем саму эту функцию.

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

Название параметру - sortField дали произвольное.


onSort = sortField => (
    console.log(sortField)
  )


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



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

Собственно сортировка.

Для упорядочивания элементов по алфавиту и в цифровом порядке, мы обратимся к orderBy Lodash.

Добавим ее в наш проект.

npm i --save lodash


После того, как завершится установка, в зависимостях (package.json) появится нужная нам библиотека:



Теперь нам нужно ее импортировать. Для этого идем в файл App.js и добавляем следующую строку:

import _ from 'lodash';

В методе onSort создадим копию нашего массива:

const cloneData = this.state.data.concat();

Это делаем для того, чтобы у нас никак не менялся state.data в состоянии компонента.

Теперь нам нужно отсортировать эту копию массива по определенному полю, которое мы передаем в функцию в виде аргумента - sortField и в определенном направлении

Направление мы сохраним в стейтах как sort:'asc'. Стоит помнить, что оно может меняться на 'desc'


  state ={
    isLoading: true,
    data: [],
    sort: 'asc',  // 'desc'
    sortField: 'id', // поле по умолчанию
  }


Теперь нам нужно определить направление. Назовем переменную - sortType и проверим, что у нас лежит в свойстве sort наших стейт. И если значение переменной будет 'asc', то мы меняем его на 'desc' и наоборот.

const sortType = this.state.sort === 'asc' ? 'desc' : 'asc';

Создадим переменную для отсортированного массив - orderedData. Метод сортировки напишем по примеру документации. передадим туда копию нашего массива - cloneData, вторым параметром нам нужно указать поле по которому мы сортируем - sortField и направление - sortType

const orderedData = _.orderBy(cloneData, sortField, sortType);

В итоге мы получаем отсортированные данные и нам остается изменить состояние нашего компонента - this.setState({}).

Здесь важно определить поля, которые мы будем менять.

Поле data мы меняем на orderedData, sort на sortType и поле sortField на sortField.

sortField нужно для того чтобы мы знали какое поле мы сортировали. Поэтому добавили его в наши стейт с значением 'id' по умолчанию.

Метод onSort компонента App.js полностью:

  onSort = sortField => {
    
    const cloneData = this.state.data.concat();
    const sortType = this.state.sort === 'asc' ? 'desc' : 'asc';
    const orderedData = _.orderBy(cloneData, sortField, sortType);

    this.setState({
      data: orderedData,
      sort: sortType,
      sortField
    })
  }



Теперь можно посмотреть на результат в браузере. Нажимаем на LastName и происходит сортировка в этом столбце по алфавиту.



Еще раз нажимаем и сортировка начинается в обратном порядке:



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

Визуальное отображение сортировки.

Во первых мы будем передавать в компонент Table два параметра из стейта компонента App. За направление отвечает -sort и за поле - sortField. При изменении сортировки они будут меняться в методе this.setState({}), который мы написали ранее.


      {
        this.state.isLoading 
        ? <Loader />
        : <Table 
        data={this.state.data}
        onSort={this.onSort}
        sort={this.state.sort}
        sortField={this.state.sortField}
        />
      }


Мы их передали в наш компонент и теперь мы их проверим.

{props.sortField === 'id' ? <small>{props.sort}</small> : null}

Проще говоря, если в sortField нам будет передан 'id', то мы покажем некий компонент - <small>{props.sort}</small> на экране (в данном случае в поле заголовка - ID), а если нет, то ничего не покажем - null.

Такие конструкции, только с соответствующими значениями мы добавим во все поля заголовка таблицы:


<thead>
  <tr>
      <th onClick={props.onSort.bind(null, 'id')}>
          ID {props.sortField === 'id' ? <small>{props.sort}</small> : null}
      </th>
      <th onClick={props.onSort.bind(null, 'firstName')}>
          First Name {props.sortField === 'firstName' ? <small>{props.sort}</small> : null}
      </th>
      <th onClick={props.onSort.bind(null, 'lastName')}>
          Last Name {props.sortField === 'lastName' ? <small>{props.sort}</small> : null}
      </th>
      <th onClick={props.onSort.bind(null, 'email')}>
          E-mail {props.sortField === 'email' ? <small>{props.sort}</small> : null}
      </th>
      <th onClick={props.onSort.bind(null, 'phone')}>
          Phone {props.sortField === 'phone' ? <small>{props.sort}</small> : null}
      </th>
  </tr>
</thead>



Посмотрим в браузере. Если нажать на заголовок - ID, то появится метод сортировки - (asc), а при повторном нажатии - (desc)



Единственная проблема, что при перезагрузки страницы, мы по умолчанию в поле ID показываем сортировку asc, хотя в действительности никакой сортировки еще нет.

Поэтому, чтобы не путать пользователя, мы в компоненте App, в момент получения данных в сет-стейт метода componentDidMount() добавим сортировку:


  async componentDidMount() {
    const response = await fetch(` http://www.filltext.com/?rows=32&id={number|1000}&firstName={firstName}&lastName={lastName}&email={email}&phone={phone|(xxx)xxx-xx-xx}&address={addressObject}&description={lorem|32}`)
    const data = await response.json()
   
    this.setState({
      isLoading: false,
      data: _.orderBy(data, this.state.sortField, this.state.sort)
    })

  }



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

Сейчас мы можем проверить сортировку по всем полям.



На этом этапе код по ссылке: react-table-abc
ci -m "Sort items in columns and display sorting options in the header."

Как задание для тренировки: вы можете заменить строку <small>{props.sort}</small> на отдельный компонент в котором вывести отдельно индикаторы сортировки в виде картинок или иконок.

Вывод информации по клику.

Нам нужно сделать так, чтобы при клике по любой ячейке, строке, выводилась ВСЯ информация о пользователе на страницу, ниже нашей таблицы, отдельно.

Для этого мы в компоненте Table каждому элементу tr добавим обработчик события:

onClick={props.onRowSelect.bind(null, item)}

передадим туда item

Table.js часть:


<tbody>
  { props.data.map(item =>(
      <tr key={item.id + item.phone} onClick={props.onRowSelect.bind(null, item)}>
          <td>{item.id}</td>
          <td>{item.firstName}</td>
          <td>{item.lastName}</td>
          <td>{item.email}</td>
          <td>{item.phone}</td>
      </tr>
  ))}
</tbody>



Сейчас нам нужно написать сам метод в компоненте App и передать его в Table Передаем его в компонент:


{
  this.state.isLoading 
  ? <Loader />
  : <Table 
  data={this.state.data}
  onSort={this.onSort}
  sort={this.state.sort}
  sortField={this.state.sortField}
  onRowSelect={this.onRowSelect}
  />
}


Выше метода render в компоненте App пишем сам метод, который будет принимать в качестве параметра row и выведем его в консоль:


  onRowSelect = row => (
    console.log(row)
  )



Проверим, что же у нас будет в консоли?



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


  state ={
    isLoading: true,
    data: [],
    sort: 'asc',  // 'desc'
    sortField: 'id',
    row: null,
  }


И вместо вывода в консоль данных объекта в методе onRowSelectсделать изменение стейта:


  onRowSelect = row => (
    this.setState({row})
  )


Теперь в этом же компоненте мы будем выводить детальное отображение этой строчки. Для этого нам потребуется новый компонент DetailRowView и некоторая логика:


      {
        this.state.row ? <DetailRowView person={this.state.row} /> : null
      }


Если в this.state.row что-то есть, то покажем новый компонент, а если нет, то ничего не покажем.

В компонент мы передадим состояние.

Создадим сам компонент

В папке src создадим папку- DetailRowView с одноименным js-файлом. Создадим простой компонент и выведем какой-либо текст на экран:


import React from 'react';

export default () => (
   <div>
     <h1>DetailRowView</h1>
   </div>
)


Не забываем импортировать этот компонент в App.js

import DetailRowView from './DetailRowView/DetailRowView';

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



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

Данные из props получим сразу путем деструктуризации. ({props})

Теперь нам стоит посмотреть на то, что получает данный компонент в консоли на вкладке Networks



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

DetailRowView.js

import React from 'react';

export default ({person}) => (
    <div>
    <p>Выбран пользователь <b>{person.firstName + ' ' + person.lastName}</b></p>
    <p>
    Описание: <br />
    <textarea defaultValue={person.description} />
    </p>

    <p>Адрес проживания: <b>{person.address.streetAddress}</b></p>
    <p>Город: <b>{person.address.city}</b></p>
    <p>Провинция/штат: <b>{person.address.state}</b></p>
    <p>Индекс: <b>{person.address.zip}</b></p>

  </div>
)


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



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

На этом этапе код по ссылке: react-table-abc
ci -m "data output from the table row to the page"

Продолжение ->

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


Удачного кодирования!                                                                                                                                                              

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


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