React Nedir? Ne Zaman İhtiyaç Duyarız?

User interfaces oluşturmak için JS kütüphanesi

React düzenli DOM manuplasyonu yapılan sitelerde kullanılır. Bütün sayfa render edilmeden sadece ilgili alan render edilir.

Real DOM

Web sayfasını oluşturan html etiketlerinin tamamı Real DOM'u oluşturur.

Vitrual DOM

real DOM yapısının js üzerinde obje olarak tutulan kopyası.

Veri güncellendiğinde react vitrual DOM ve real DOM'u karşılaştırır. Fark var ise sadece fark olan yer real DOM üzerinde değiştirilir. Bu sayede tüm sayfa render edilmemiş olur.

ES6 Modül Sistemi

Modül sistemini çalışması için package.json içine "type": "module" verisi girilmeli.
Bunu yazdıktan sonra daha önce require ile çağırdığımız modülleri artık farklı bir şekilde import edeceğiz.
Her iki import şekli aynı anda çalışmaz. Modül aktif edildiğinde require hata verir.

const slugify = require('suligify'); 
              

yerine

import slugify from "slugify"
              
kullanılır.

Modülü export ederken de iki sistem arasında yazım farkları mevcut.
Modüle aktif değilken:

exports.topla = (a, b) => {console.log(a + b);};
exports.hello = () => {console.log("hello")};
              
kullanılır. import için de
const {hello, topla} = require("../js/myModule")
kullanılır.

Modüle aktifken:
export const topla = (a, b) => {console.log(a + b);}; 
export const hello = () => {console.log("hello")}; 
              
veya
const topla = (a, b) => {console.log(a + b);}; 
const hello = () => {console.log("hello")}; 
export {topla, hello}
              
kullanılır.

import için de:
import {topla, hello} from "../js/myModule"
kullanılır.

modül kullanımında tek bir fonksiyon default olarak dışa aktarılabilir.

export const topla = (a, b) => {console.log(a + b);};
export const hello = () => {console.log("hello")}; 
const cikar = (a, b) => {console.log(a - b);}; 
export default hello;
              
default olarak gönderilen fonksiyon çağırılırken süslü paranteze konmaz.
import cikar, {topla, hello} from "../js/myModule"
              

default fonksiyon çağırılırken oluşturulan ismi dışında bir isimle de çağırılabilir.
import app, {topla, hello} from "../js/myModule"
              
bu yazımda cikar fonksiyonu çağırıldığı yerde app adı ile kullanılır.

her iki import yapısıyla da fonksiyon dışında diğer değişkenler (sting, array, object vs) de import-export edilebilir.

Callback Functions / async-await

İşimize yarayacak fonksiyonlar.

setTimeout() belirli bir süre sonunda içine tanımlanan fonksiyonun gerçekleşmesini sağlar. 2 parametre alır. ilk parametreye fonksiyon ikinciye milisaniye cinsinden süre yazılır.
setInterval() belirli bir sürede tanımlanan fonksiyonun tekrar tekrar gerçekleşmesini sağlar. 2 parametre alır. ilk parametreye fonksiyon ikinciye milisaniye cinsinden süre yazılır.
fetch() herhangi bir veri kaynağına bağlanıp aldığı veriyi bize getirir.

Callback: başka bir kod parçasına argüman olarak iletilen yürütülebilir koda yapılan herhangi bir başvurudur. Bir fonksiyon çalışmasını tamamladıktan sonra başka bir fonksiyonun çalışmasını sağlayan fonksiyonlara callback fonksiyon denir.

js yapısı gereği fonksiyonlar birbirinin tamamlanmasını beklemeden devreye girerler. Bunu olmasını engellemek istediğimiz fonksiyonlar ya .then yapılarıyla yada async-await ile yazılır.

.then örneği:

fetch("https://jsonplaceholder.typicode.com/users/1") 
.then((data) => data.json()) 
.then((users) => { 
  console.log("users yüklendi", users); 

  fetch("https://jsonplaceholder.typicode.com/posts/1") 
    .then((data) => data.json()) 
    .then((posts) => console.log("post yüklendi", posts));
});
              
Bu yapıda her .then() kendinden önceki fonksiyonu bekler ve ondan gelen veriyi alır.

async-await örneği

async function getData() { 
  const user = await ( 
    await fetch("https://jsonplaceholder.typicode.com/users/1") 
  ).json(); 

  const post = await ( 
    await fetch("https://jsonplaceholder.typicode.com/posts/1") 
  ).json(); 

  console.log("users yüklendi", user); 
  console.log("post yüklendi", post); 
} 

getData(); 
              
Bu yapıda asenkron olması için fonksiyonun başına async eklenir. Tamamlanması beklenecek her fonksiyonun başına da await eklenir. Sonra da fonksiyon çağırılır.

Fonksiyonu isimlendirmek ve ayrıca çağırmak istemediğimiz durumlarda anonim fonksiyon yapısı kullanılır.
(()=>{})(); Örnek:

(async()=>{ 
  const user = await ( 
      await fetch("https://jsonplaceholder.typicode.com/users/1") 
    ).json(); 
  
    const post = await ( 
      await fetch("https://jsonplaceholder.typicode.com/posts/1") 
    ).json(); 
  
    console.log("users yüklendi", user); 
    console.log("post yüklendi", post); 
})();
              

hoca fetch işlevi için npm den node-fetch kurdurup fetch adıyla import edilir diyor ama paket indirmeden de fetch komutu çalışıyor. node versiyonu ile ilgili olabilir.

axios kütüphanesi fetch'in yaptığı işi daha kolay yapmamızı sağlıyormuş.
kodun axios hali:

(async()=>{ 
  const {data: user} = await axios("https://jsonplaceholder.typicode.com/users/1") // {data: user} axios tarafından gelen data değişkeninin adını user yapar.              
  const {data: post} = await axios("https://jsonplaceholder.typicode.com/posts/1") 

    console.log("users yüklendi", user); 
    console.log("post yüklendi", post); 
})();
              

Promises

.then() fonksiyonu ile data alınabilen fonksiyonlara promise denir.
.then() ile olumlu sonuç (resolve) yakalanır.
.catch() ile olumsuz sonuç (reject) yakalanır.

Örnek 1:

const getComment = (number) => {
  return new Promise((resolve, reject)=>{ // bu satırda bir promise başlatıyoruz. promise 2 parametre alır.
      if(number === 1){ 
resolve('comments'); // Bu kısım .then() ile yakalanır.
} reject('bir problem var') // Bu kısım .catch() ile yakalanır. }) } getComment(1) .then((data) => console.log(data)) .catch((e)=> console.log(e))

Örnek 2

const getUser = (userId) => { 
  return new Promise(async(resolve, reject)=>{ 
      const user = await ( 
              await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)) 
              .json();  
      resolve(user) 
  })
}

getUser(2) 
.then((data) => {console.log(data); return data.name}).then((data) => console.log(data)) 
.catch((e)=> console.log(e))
              

veya

const getUser = (userId) => { 
  return new Promise(async(resolve, reject)=>{ 
      const user = await ( 
              await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)) 
              .json();  
      resolve(user) 
  }) 
} 

(async () => { 
  try { 
    const user = await getUser(2); 
    console.log(user); 
  } catch (err) { 
    console.log(err); 
  } 
})();
              
Burada kullanılan try-catch bloğu hata yakalamak için kullanılır.

Birden fazla promise yapısında fonksiyonu çalıştırmak için

Promise.all()
içine array olarak çalışması istenen fonksiyonların adı yazılır.
Promise.all([getUsers(), getPost(1)]) 
.then(console.log) 
.catch(console.log)
              

Array Fonksiyonları

push, map, find, filter, some, every, includes

push

Örnek

const users = ["Mehmet", "Ahmet". "Murat"] 
users.push("Ayşe") 
console.log(users)
              

çıktı:

["Mehmet", "Ahmet". "Murat", "Ayşe"]
              

map

For döngüsü gibi array içindeki tüm değerlerde dönüyor.

Örnek

const users = ["Mehmet", "Ahmet". "Murat"] 
users.map((x)=>{console.log(x)}) 
              

çıktı:

Mehmet 
Ahmet 
Murat
              

find

Array içinde arama yapar. Koşula uyan bir şey varsa ilk bulduğunu getirir. Yoksa undefined döner. Aramada mantık operatörleri kullanılabilir.

Örnek:

const users = [ 
{ 
  name: "Mehmet",
  age: 18
}, 
{
  name: "Mehmet",
  age: 25
}, 
{
  name: "Murat",
  age: 28
}, 
] 
const result = users.find((x)=> x.name === "Mehmet" && x.age > 20) 
console.log(result)
              

çıktı:

{ name: 'Mehmet', age: 25 }
              

filter

Filtreleme yapar.

Örnek:

const users = [ 
{ 
  name: "Mehmet",
  age: 18
}, 
{
  name: "Mehmet",
  age: 25
}, 
{
  name: "Murat",
  age: 28
}, 
] 
const filtered = users.filter((x)=> x.name === "Mehmet" && x.age > 10) 
console.log(filtered)
              

veya

const filtered = users.filter(({ name, age })=> name === "Mehmet" && age > 10) 
console.log(filtered)
              

Çıktı:

[ 
{ name: 'Mehmet', age: 18 }, 
{ name: 'Mehmet', age: 25 }  
]
              

some

Array içindeki değerlerden biri koşula uyuyorsa true, hiçbiri uymuyorsa false döner

Örnek:

const users = [ 
{ 
  name: "Mehmet",
  age: 18
}, 
{
  name: "Mehmet",
  age: 25
}, 
{
  name: "Murat",
  age: 28
}, 
{
  name: "Hasan",
  age: 10
}, 
] 
const some = users.some((x)=> x.age = 10) 
console.log(some)
              

çıktı:

true
              

every

Array içindeki değerlerin hepsi uyuyorsa true, biri bile uymuyorsa false döner

Örnek:

const users = [ 
{ 
  name: "Mehmet",
  age: 18
}, 
{
  name: "Mehmet",
  age: 25
}, 
{
  name: "Murat",
  age: 28
}, 
{
  name: "Hasan",
  age: 10
}, 
] 
const every = users.every((x)=> x.age > 9) 
console.log(every)
              

çıktı:

true
              

includes

Arrayin içinde olma durumunu verir. Varsa true, yoksa false verir.

Örnek:

const meyveler = ["elma", "armut", "muz"]; 
const isIncluded = meyveler.includes("muz"); 
console.log(isIncluded)
              

çıktı:

true
              

Bir React Projesini Ayağa Kaldırmak (create-react-app)

create-react-app facebook'un hazırladığı ve paylaştığı hazır bir react geliştirme ortamı. github.com/facebook/create-react-app

crate-react-app için npx kurmak gerekiyor. Bu da node yüklenirken bilgisayara yüklenmiş oluyor. npx, npm de global olarak kurulup kullanılması gereken paketlerin kurulmadan kullanılmasına olanak sağlıyormuş.

create-react-app kurulumu için terminale:

npx create-react-app my-example-app
              
my-example-app yerine kendi projemizin adını yazıyoruz. Bunun sonucunda terminalin içinde olduğu dizine projemizin adında bir klasör oluşturuluyor. my-example-app yerine nokta (.) koyarsak bulunduğumuz dizinin içine kurar.

projeyi açmak için terminale:

cd my-example-app
              
(bizim klasörümüzün adı neyse o yazılacak) ile klasörün içine girip sonra:
npm start
              
ile uygulama başlatılır. Açıldığında bizi http://localhost:3000/ adresinde karşılar. 3000 portu dolu ise başka bir portta açar. Açtığı portu terminalde gösterir. Hatta varsayılan tarayıcıda da açar.

Component Nedir?

Web sayfasını meydana getiren bileşenlerdir. Bu companentler birleşerek başka kompanentleri ve sonunda web sayfamızı oluştururlar.

Component Oluşturmak/Kullanmak

create-react-app işlemi sonrası bizim için kurulan dosyalar:
node_modules: bize react için gereken node modulleri
public:
source: bizim için asıl önemli kısım:

  • App.js bundan sonra yazacağımız bütün kompanentleri bu app kompanentinde birleştirerek ilerleyeceğiz.

Companent oluşturmak için companentin adında bir fonksiyon oluşturmak ve return ile istediğimiz companent htmlsini yazmamız yeterli

public/index.html içinde id si root olan bir div mevcut.
src/index.js içinde 'react' ve 'react-dom' import edilmiş. './index.css' imort edilmiş. App modülü ve reportWebVitals modülü de import edilmiş. devamında

const root = ReactDOM.createRoot(document.getElementById('root'));
              
ile index.html içinde id='root' olan elemente ulaşıyor.
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
              
bu elemente app companentini yerleştiriyor.

App.js içindeki kompanenti değiştirdiğimizde sayfada görünen kısım da değişiyor.

Kendi kompanentimizi yazmak:
src içine components/Header.js oluşturuldu. içine:

function Header(){ 
  return( 
    <div> 
        Merhaba Ben Header Bileşeniyim. 
    </div> 
  ); 
} 
import default Header; 
              
yazıldı.
App.js içine import edildi ve kullanıldı.
import './App.css';
import Header, {  } from "./components/Header";

function App(){
  return (
    <div>
       <Header />
    </div>
  );
}

export default App;
              

JSX ve Temel Kuralları

JavaScript’in bir söz dizimi uzantısıdır. Bu olmadan da react yazılabilir ama kod çok karmaşıklaşır. JSX sayesinde html yazar gibi js kodları yazabiliyoruz.

JSX kuralları

Kompanent belirtilen fonksiyon adının büyük harf olmasına dikkat edelim. Bu sayede html etiketleri ile karışmazlar. Küçük harfle başlarsa react yorumlarken kompanenti html etiketi olarak yorumlar.

Her kompanent eklenirken bir kapsayıcı etiket içine dahil edilir.
<div></div> veya boş etiket "<> </>" kullanılabilir.

JS için özel tanımlı keywordler kullanılmaz.
class ifadesi js içinözel tanımlı. yerine classname kullanılır.
for yerine htmlFor kullanılır. vs
Bu kullanım sayedinde js'de de anlamı olan keywordler jsx içinde kullanılmamış olut.

JSX içinde değişken kullanma:

İki şekilde yapılabilir.

Değişken süslü parantez içine alınabilir.

... 
  <h1>Benim Adim {name}</h1> 
... 
              
veya süslü parantez arasına backtick ile yazılabilir.
... 
 <h1>{`Benim adım: ${name}`}</h1> 
... 
              

Koşullu Render

Koşulu sorgulama için isLoggedIn adında boolean değerli bir değişken oluşturduk.

... 
<h1>{isLoggedIn && `Benim adım: ${name}`}</h1> <h1>{!isLoggedIn && `Giriş Yapmadınız`}</h1> ...

veya

... 
  <h1>{isLoggedIn ? `Benim adım: ${name}` : `Giriş Yapmadınız` }</h1> 
... 
                

Props Nedir? Nasıl Kullanılır?

Props (properties) kompanent içinde parametre geçebileceğimiz bir yapı. html etiketlerindeki Attribute-value gruplarına benzer şekilde veri App.js üzerinde kullanılan componente girilerek componentin işlemesi sağlanır.

App.js içinde:

<div> 
  <User name="Murat" surname="Gökduman" isLoggedIn={false}/> 
</div> 
              
ile props olarak bilgiler gönderilir. props gönderildiği yerde işlenir ve User componenti olarak kullanılır.

User.js içinde:

  function User(props){ 
    return( 
        <div> 
            {props.isLoggedIn ? `${props.name} ${props.surname}`: "Giriş Yapmadınız"}            
        </div> 
    ) 
} 

export default User 
              
kuralına uygun olarak veri işlenir ve gönderilir. Bu kodda props obje yapısında olduğundan:
  function User({name, surname, isLoggedIn}){ 
    return( 
        <div> 
            {isLoggedIn ? `${name} ${surname}`: "Giriş Yapmadınız"}            
        </div> 
    ) 
} 

export default User  
              
olarak da yazılabilir.

props gönderme ve alma sırası önemli değil.

Döngülerde "key" Prop'u

Kompanente prop olarak bir array ekleyip görüntüleyeceğiz.

Array listelenen durumlarda her elemanını üstündeki etikette unique bir key değeri belirtilmeli. Bunun için array map metodundan alınan index değeri kullanılabilir.

ket değeri reactın performansı için gerekli. Yzılmazsa konsola uyarı verir.

Daha önceki örnekte kullandığımız User kompanentine App.js içinde bir array prop olarak girildi:

friends={["Ahmet", "Tayfun", "Gökhan", "Ayşe", "Fatma"]}
              

User.js içinde .map() metodu ile tek tek çağırıldı.
  {friends.map((friend, index) => ( 
    <div key={index}> 
        {index} - {friend} 
    </div> 
))} 
              
key olarak index kullanıldı.

Kullandığımız array obje yapısındaysa ve unique bir id değeri varsa bu değer de kullanılabilir.

örn:
App.js içinde:

const friends = [{name: "Ahmet", id: 1}, {name: "Tayfun", id: 2} , {name: "Gökhan", id: 3} , {name: "Ayşe", id: 4}, {name: "Fatma", id: 5}] 
... 
friends={friends} 
... 
              
User.js içinde:
{friends.map((friend) => ( 
  <div key={friend.id}> 
  {friend.id} - {friend.name} 
  </div> 
  ))} 
                

veya

{friends.map((friend) => { 
  return (<div key={friend.id}> 
  {friend.id} - {friend.name} 
  </div>)  
})}
                
Bu yapı özellikle fonksiyon içinde başka bir işlem de yapılacaksa faydalıdır.

React Developer Tools

chrome extansion: React Developer Tools

Sayfayı sağ tıklayıp incele dediğimizde çıkan menüye Components sekmesi ekler. Bu sayede companentleri ve propslarını sayfamızda ayırdedebiliriz. Hiyerarşik olarak gösterir. Anlık olarak değiştirip değişikliğin sonucunu da deneyebiliriz.

Prop Types

Kompanentin hangi prop tiplerini kabul edeceğini ayarlamak için kullandığımız bir araç.

User kompanenti için User.js içine:

import PropTypes from "prop-types";
              
ile "prop-types" import edilir.
  User.propTypes = { 
    name: PropTypes.string, 
    surname: PropTypes.string, 
    isLoggedIn: PropTypes.bool, 
    friends: PropTypes.array 
    age: PropTypes.number 
}
              
ile prop tipleri belirtilir.

isRequired

Gönderilecek propun zorunlu olduğunu belirtmek için:

... 
  name: PropTypes.string.isRequired, 
... 
              

oneOfType

Bir prop için birden fazla veri tipi kabul etmek için:

... 
  age: PropTypes.oneOfType([ 
    PropTypes.number, 
    PropTypes.string 
  ]), 
... 
              

shape

Obje olarak gönderilen prop için kullanılabilir.

  ... 
  adress: PropTypes.shape({ 
    title: PropTypes.string, 
    zip: PropTypes.number, 
}) 
... 
              

Default Props

İlgili kompanent içinde özellikle prop değeri belirtilmemiş proplar için de default prop değeri atanabilir.

User.defaultProps = { 
  isLoggedIn: false, 
} 
              
Prop olarak değer gelirse gelen değeri, gelmezse default değeri kullanır.

State Nedir? Nasıl Oluşturulur?

State kompanentler üzerinde değerinin değişme potansiyeli olan bütün değerleri tutan JS objesidir. State değişince ilgili kompanentdeki değişim ekrana render edilir.

import { useState } from "react"; // useState yapısı react içinden import edilir.
              
function App(){ 
  const [name, setName] = useState("Murat") //fonksiyon içine ilgili state tanımlanır. useState içindeki değer default değerdir.
  ...

    return ( 
    <div> 
        <h1>Hello {name} </h1>  {/* return içinde değişken gibi kullanılır. */}

        <button onClick={() => setName("Ahmet")}>Click</button> {/* setname fonksiyonu ile değer değiştirilir. değiştirilmesini sağlayan bir işlem sonrası yeni hali render edilir. */}
  </div> 
  ); 
}    

export default App;
              

state değeri js içinde kullanılacak tüm değişkenleri içerebilir.

Herhangi bir state güncellendiği anda render işlemi baştan yapılır.

Array States

App.js App fonksiyonunda daha önceki stateleri eklediğimiz yere:

const [friends, setFriends] = useState(['Ahmet', 'Murat']);
              
ile array yapıda state eklenir.
App fonksiyonunun return parantezinin içindeki kapsayıcı div etiketinin içine:
<h2>Friends</h2> 
  { 
    friends.map((friend, i) => ( 
      <div key={i}>{friend}</div> 
    )) 
  }
              
ile arrayin render edileceği fonksiyon yazılır.
<button onClick={() => setFriends([...friends, 'Ayşe'])}>add new friend</button>
              
setFriends içindeki yapı ilk tanımdaki gibi array olmalıdır. Buna dikkat edilmezse arrayi yansıtırken kullanılacak kodlarda hata alırız.

array içindeki ...friends ifadesi mevcut arrayin korunmasını sağlar. Yeni ifade bunun sonuna yazılırsa sonuna, öncesine yazılırsa başına eklenir.

Mevcut ifadeyi korumanın bir başka yolu:

setFriends((prevState) => [...prevState, 'Ayşe'])
              
ile önceden eklenen kısım çağırılıp dizine eklenebilir.

Object States

App.js App fonksiyonunda daha önceki stateleri eklediğimiz yere:

const [address, setAddress] = useState({ title: 'ev', zip: 34765, city: 'Ankara' });
              
ile object yapıda state eklenir.
App fonksiyonunun return parantezinin içindeki kapsayıcı div etiketinin içine:
<h2>Address</h2> 
<div>{address.title} {address.zip} {address.city}</div>
              
ile objenin render edileceği fonksiyon yazılır.
<button onClick={() => setAddress({...address, title: 'iş', zip: 34344})}>Change Address</button>
              
setAddress içindeki yapı ilk tanımdaki gibi object olmalıdır. Buna dikkat edilmezse objeyi yansıtırken kullanılacak kodlarda hata alırız.

obje içindeki

...address
ifadesi mevcut address bilgilerinin default olmasını sağlar. Bu sayede yeni tanımlanmayan değerler ilk tanıma bağlı kalır.
...address
olmazsa value değeri alamayan keyler silinir.

Mevcut ifadeyi korumanın bir başka yolu:

setAddress((prevState) => {...prevState, title: 'iş', zip: 34344})
              
ile önceden eklenen kısım çağırılıp dizine eklenebilir.

Sayaç Uygulaması

src/components içinde Counter.js dosyası oluşturulur. İçine:

import React, { useState } from 'react'
              
ile useState import edilir.
function Counter() { 
  const [count, setCount] = useState(0)
              
ile state oluşturulur.
  return ( 
    <div> 
        <h1>{count}</h1>
              
ile state yerleştirilir.
        <button onClick={() => setCount(count+1)}>Decrease</button> 
        <button onClick={() => setCount(count-1)}>Increase</button>
              
ile butonlara setCount fonksiyonu verilir.
    </div> 
  )
} 

export default Counter
              
ile export edilir.

App.js içinde veya doğrudan index.js içinde import edilerek kullanılır.

onClick fonksiyonlar başka yerde tanımlanıp daha sonra onClick içinde kullanılabilir.

function Counter() { 
  const [count, setCount] = useState(0) 

  const increase = () => {
      setCount(count-1)
  } 

  const decrease = () => {
      setCount(count+1)
  } 
  return ( 
    <div> 
        <h1>{count}</h1> 
        <button onClick={decrease}>Decrease</button> 
        <button onClick={increase}>Increase</button> 
    </div> 
  )
}
              

Input için State Tanımı Yapmak

src/components içine InputExample.js oluşturulud. içine:

import { useState } from "react" 

function InputExample() { 
    const [name, setName] = useState('test') br
    return( 
        <div> 
            Please enter a name: <br /> 
            <input value={name} onChange={(event)=>setName(event.target.value)}/>
              
ile formda değişiklik yapıldığında setName işlemi çağırılır. Forma yazılan value event.target.value yakalanır.
            <br /> 
            {name} 
        </div> 
    ) 
} 

export default InputExample 
              

onchange içindeki fonksiyonu dışa taşımak için:

import { useState } from "react" 

function InputExample() { 
    const [name, setName] = useState('test') 
    const nameChange = (event)=>setName(event.target.value)
              
fonksiyon buraya tanımlanır ve içeride kullanılır.
    return( 
        <div> 
            Please enter a name: <br /> 
            <input value={name} onChange={nameChange}/> 

            <br /> 
            {name} 
        </div> 
    ) 
} 

export default InputExample 
              

Birden fazla veri alıcak formlarda state'i object olarak vermek daha kullanışlı olacaktır. Bu sayede tek fonksiyon ile tüm formlarda onChange'den veri alınabilir.

import { useState } from "react" 

function InputExample() { 
    const [form, setForm] = useState({name:'', surname:''})
              
ile form statei içine name ve surname keyleri boş value ile atanır.
    const formChange = (event)=> setForm({...form, [event.target.name]: event.target.value})
              
ile formdaki değişiklikleri takip edecek fonksiyon yazılır. state objenin key değeri formdaki name değeri ile aynı olmalıdır.
    return(  
        <div> 
            Please enter a name: <br /> 
            <input name='name' value={form.name} onChange={formChange}/> 
            <br /> 
            Please enter a surnamename: <br /> 
            <input name='surname' value={form.surname} onChange={formChange}/> 

            <br /> 
            {form.name} {form.surname} 
        </div> 
    )
} 

export default InputExample 
              

useEffect

kompanenetler DOM'a mount olduğu anda, state değiştiğinde, prop değiştiğinde veya unmount olduğunda bu durmları yakalayıp işlem yaptırabiliyoruz.

kompanent içinde herhangi bir ifade değiştiğinde useEffect() çalışır.

useState(), useEffect() gibi hooklar kompanentin en başında olmalı ve herhangi bir koşul yapısıda olmamalı.

import { useState, useEffect } from "react";
              
ile useState ve useEffect import edilir.
function App(){ 

  const [age, setAge] = useState(33); 
  const [name, setName] = useState('Murat'); 
  
  useEffect(()=>{ 
    console.log('state güncellendi') 
  })
              
herhangi bir state güncellendiğinde çalışır.
  useEffect(()=>{ 
    console.log('Component mounted') 
  }, []);
              
[] bu şekilde boş ise komponentin mount edildiği anda çalışır.
  useEffect(()=>{ 
    console.log('Age component update') 
  }, [age]);
              
age komponenti güncellendiği anda çalışır.
  useEffect(()=>{ 
    console.log('Age veya name component update') 
  }, [age, name]); 
              
age veya name komponenti güncellendiği anda çalışır.
  return ( 
    <div> 
        
        <h1> 
        Age: {age} <br /> 
        <button onClick={() => setAge(age + 1)}>+</button> 
        <button onClick={() => setAge(age - 1)}>-</button> 
        </h1> 
        <h1> 
        Name: {name} <br /> 
        <button onClick={() => setName('Ahmet')}>değiş</button> 
        </h1> 
        
    </div> 
  );
} 

export default App;
              

Component Unmount

src/components içinde Counter.js adında yeni dir dosya oluşturuyoruz ve useEffect ve useState fonksiyonunu import ediyoruz.

function Counter() { 
  const [count, setCount] = useState(0) 

  useEffect(()=>{ 
    console.log('count state update') 
  },[count]) 

  useEffect(()=>{ 
    console.log('componet mounted'); 
    const interval = setInterval(()=>{ 
      setCount((n) => n+1 
    )}, 1000); // bu kısım component mount edildiğinde çalışmaya başlar. 

      return () => clearInterval(interval) // useEffect içindeki returndan sonraki kısım component unmount edildiğinde çalışır. Bizim örneğimizde unmount olduğunda intervali durdurur. 
},[]) const increase = () => { setCount(count-1) } const decrease = () => { setCount(count+1) } return ( <div> <h1>{count}</h1> <button onClick={decrease}>Decrease</button> <button onClick={increase}>Increase</button> </div> ) } export default Counter

useState ve yeni oluşturduğumuz Counter kompanenti App.js içine import edilir ve sonra:

function App() { 
  const [isVisible, setIsVisible] = useState(true) 
  return ( 
    <div> 
      {isVisible && <Counter />} 
      <button onClick={() => setIsVisible(!isVisible)}>change visible</button> {/* ile butona her basıldığında isVisible değeri değişir. Buna bağlı olarak da Counter mount veya unmount edilir. */}
    </div> 
); }
export default App;

Contacts App

terminale npx create-react-app contacts-app ile yeni bir proje başlattık.

src/components/Contacts dosyası oluşturuldu. içine index.js oluşturuldu.

.../Contacts/Form ve .../Contacts/List dosyaları oluşturuldu ve her birinin içine index.js oluşturuldu. her iki index.js içinde de birer component tanımlandı ve bunlar .../Contacts/index.js içine import edildi.

.../Contacts/index.js içinde bir component oluşturuldu ve içinde import edilen diğer iki component kullanıldı. Oluşan bu component de App.js içinde import edilip kullanıldı.

Bu noktaya kadar yazılan componentlerin içine sadece temsili bilgiler girildi.

Form Component'inin Geliştirilmesi

Form/index.js içindeki kompanenetin return kısmında bir form içinde iki tane input bir tane de buton oluşturduk.

kompanentin içinde bir form adında obje özellikli bir state oluşturduk. Bu objedeki keyler ile input name değerleri aynı adı taşıyor.

inputlardan gelen event.target.value değerini setForm ile alması için bir fonksiyon yazıp input onChange özelliğine dahil ettik.

Contacts/index.js içinde contacts adında array bir state oluşturup hem kendisini hem de değiştirme fonksiyonunu props olarak Form/index.js deki fonksiyona gönderdik.

Gelen değiştirme fonksiyonunu form submit sırasında kullanılacak bir fonksiyona dahil ettik. Bu sayede formdan gelen bilgiyi contacts içine ekledik.

submit sonrası formu temizlemek için setForm fonksiyonunu kullandık. Bunu ister submt içinde ister useEffect içinde kullanabiliriz.

Form/index.js son hali:

import { useEffect, useState } from "react"; 

function Form({ addContacts, contacts }) { // ile props olarak gelen state karşılanır. 
              
  const [form, setForm] = useState({ fullname: "", phone_number: "" }); 

  useEffect(()=>{ 
    setForm({ fullname: "", phone_number: "" }) 
  },[contacts]) // ile form içeriği sıfırlanır. 

  const onChangeInput = (e) => { 
    setForm({ ...form, [e.target.name]: e.target.value }); 
  }; // inputa değer girildiğinde form statine dahil eden fonksiyon. input içinde onChange ile kullanılıyor. 
  
  const onSubmit = (e) => { // form submit edildiğinde kullanılan fonksiyon. 
    e.preventDefault(); // formun submit sırasında kullandığı default işlemi engeller. Bu olmazsa submit işlemi adres çubuğunda query ile sonlanır.
    if(form.fullname === ''|| form.phone_number ===''){ 
      return false; 
    } // formda boş kalan yer varsa işlemi sonlandırır. 
    addContacts([...contacts, form]) 
    console.log(form); 
    // setForm({ fullname: "", phone_number: "" }) // işi biten formu sıfırlama işlemi burada da yapılabilir. 
              
  }; 

  return ( 
    <form onSubmit={onSubmit}> // onSubmit fonksiyonu burada kullanıır. 
              
      <div> 
        <input
          name="fullname"
          placeholder="Full Name"
          value={form.fullname}
          onChange={onChangeInput}
        /> 
      </div> 
      <div> 
        <input
          name="phone_number"
          placeholder="Phone Number"
          value={form.phone_number}
          onChange={onChangeInput}
        /> 
      </div> 
      <div> 
        <button>Add</button> 
      </div> 
    </form> 
  );
} 

export default Form;

              

Contacts/index.js son hali:

import {useState, useEffect} from 'react' 

import List from "./List"; 
import Form from "./Form"; 

function Contacts() { 
  const [contacts, setContacts] = useState([]); 

  useEffect(()=>{ 
    console.log(contacts) 
  },[contacts]) 
  return ( 
    <div>contacts 
        <List /> 
        <Form addContacts={setContacts} contacts={contacts}/> 
    </div> 
  )
} 
export default Contacts
              

contacts statei daha sonra listeleme işleminde de kullanılacak. Bu nedenle üst companenette oluşturulup props olarak alt companente göndeildi.

Kayıtların Listelenmesi

Contacts/index.js içinde contacts statei List companentine prop olarak gönderildi.

<List contacts={contacts}/>
              

List/index.js içinde gönderilen prop kullanıldı.

import {useState} from 'react' 

function List({ contacts }) { 
  return ( 
    <div> 
      <ul> 
        { 
          contacts.map((contact, i)=>( 
            <li key={i}>{contact.fullname}</li> 
          )) 
        } 
      </ul> 
    </div> 
  )
} 

export default List
              

Filtreleme İşlemi

İşlemi rahat takip edebilmek için Contacts/index.js içinde contacts stateine default değerler atandı.

List/index.js içinde filterText statei oluşturuldu.

Filter inputu oluşturuldu. value olarak filterText onChange fonksiyonu olarak da setFilterText atandı. Bu sayede input değeri filterText stateine atanmış oldu.

filtered adında bir değişkene contacts üzerinden filter() metodu ile filtreleme yapıldı.

return içindeki contents.map() işlemi filtered.map() olarak güncellendi.

List/index.js List companetinin güncel hali:

function List({ contacts }) { 
  const [filterText, setFilterText] = useState(""); 
  const filtered = contacts.filter((item) => { // filter() fonksiyonu array içindeki her bir item için döner. item ile bu itemler yakalanır. Fonksiyon sonunda true dönenler filtered değişkenine atanır.
    return Object.keys(item).some((key) => ( // yakalanan itemler obje yapısındadır. Object.keys(item) ile her bir key yakalanır. some() içindeki değerlerden biri true ise true döner.  
      item[key] // ile value yakalanır. 
      .toString() // ile stringe çevirilir. 
      .toLowerCase() //ile tamamı küçük harfe çevrilir. aynı işlem karşılaştırılacağı değere de yapılır. Bu sayede büyük-küçük harf duyarlılığı yaşanmaz. 
      .includes(filterText.toLowerCase()) 
    )); 
  }); 
  
  return ( 
    <div> 
      <input 
        placeholder="Filter contact" 
        value={filterText} 
        onChange={(e) => setFilterText(e.target.value)} 
      /> 

      <ul> 
        {filtered.map((contact, i) => ( // ile filtrelenen değerler yazdırılır. 
          <li key={i}>{contact.fullname}</li>  
        ))} 
      </ul> 
    </div> 
  );
}
              

Stil Tanımlarının Yapılması

css dosyası oluşturup kullanacağımız kompanentin yazıldığı js dosyasına import etmemiz gerekiyor.

import './App.css';
              
gibi.

Bundan sonraki kısım bildiğiniz css. Css kodlarının çalışması için companent içinde ilgili kısımlara class ve id atıyoruz.
örnek:

<div className="btn"> 
<ul className="list"> 
<div id='container'>
              

Stil Tanımı Yapmak

Css dosyasını import "./App.css"; şeklinde import edebiliriz.

Return içinde (JSX) inline style tanımlarını süslü parantez içine obje olarak verebiliriz.

<div style={{ color: "red", backgroundColor: "white" }}></div>
              

js içerisinde tire (-) işareti tanımlamalarda kullanılmadığından stil etiketleri camelcase olarak yazılır.
background-color yerine backgroundColor
padding-top yerine paddinTop vs

Bootstrap gibi dış kaynak eklemek için: index.html içinde head kısmına yerleştirilebilir.

Module CSS

Aynı class ismine sahip farklı companenetler için stil dosyası özellikleri çakışması sonucu stil bilgisi düzgün çalışmıyor. Her birinin kendi style.css dosyasına da tanım girilse react hepsini tek yerde topladığı için, çakışma sorunu çözülmüyor.

Burada çözüm olarak module.css kavramı devreye giriyor.

css module.css
Dosya Adı styles.css styles.module.css
Import import "./styles.css" import styles from "./styles.module.css"
className className="title" className={styles.title}

Native Fetch

Başka bir kaynaktaki veriyi alıp sayfamızda gösterme işlemi. Bunun için veriyi https://jsonplaceholder.typicode.com/ adresinden alacağız.

Önce src/components içine Users.js dosyası oluşturuldu.

içine users statei oluşturuldu. fetch işlemi ile yakalanan veri setUsers ile yakalandı ve return içinde kullanıldı.

Yükleme tamamlanana kadar loading... yazması için isLoading statei oluşturuldu ve default değer true atandı.

Koşul olarak isLoading true iken çalışacak şekilde return içinde "Loading..." yazısı eklendi.

fetch işleminin sonuna setIsLoading(false) yerleştirilerek fetch sonrası "Loading..." yazısının kalkması sağlandı.

kompanent App.js içine import edilir.

Users.js içindeki kod:

                import { useEffect, useState } from 'react' 

                function Users() { 
                  const [users, setUsers] = useState([]); 
                  const [isLoading, setIsLoading] = useState(true) 

                  useEffect(()=>{  // component mount edilirken fetch işlemi başlasın 
                    fetch("https://jsonplaceholder.typicode.com/users") // ile veri alınır. 
                    .then(res => res.json()) // ile kullanılabilir hale getirilir. 
                    .then(data => setUsers(data)) // ile users stateine set edilir. 
                    .catch((e) => console.log(e)) 
                    .finally(() => setIsLoading(false)); // ile isLoading false değeri alır. 
              
                  },[]) 

                  return ( 
                    <div> 
                      <h1>Users</h1> 

                      {isLoading && <div>Loading...</div>} {/* isLoading true ise Loading... yazar. */}
              
                      {users.map((user)=>( 
                        <p key={user.id}>{user.name}</p> 
                      ))} 
                    </div> 
                  )
                } 

                export default Users
              

Axios

fetch işlemi için kullanılan bir diğer kütüphane. fetch ile farkları için: tıklayınız

axiosda body obje olarak döner.

terminale:

npm i axios
yazılır ve kompanente import edilir.

yukarıda fetch ile yazılan kodun axios versiyonu:

useEffect(()=>{ 
axios("https://jsonplaceholder.typicode.com/users") .then(res => setUsers(res.data)) // gelen response içinde data bizim istediğimiz asıl veriyi verir. .catch((e) => console.log(e)) .finally(() => setIsLoading(false)); },[])

React Router Kurulum

reactrouter.com
v5.reactrouter.com/web/guides/quick-start

terminale:

npm i react-router-dom
              

video react-router-dom.v5 ile oluşturulmuş. Güncel versiyon react-router-dom.v6. Video ile birebir gidebilmek için terminale:

npm install react-router-dom@5
              

Biz yeni versiyon ile devam edeceğiz. Bu link çok faydalı

  • İmport ederken Switch metodu yerine Routes metodu import edilecek
  • Routes içindeki Route söz dizimi biraz daha farklı:
    <Route path="/about"><About /></Route>

    yerine
    <Route path="/about" element={<About/>}/>

Sayfaları bu şekilde tanımladığımızda bütün sayfa değil sadece değişecek kısım re-render edilir.

    React router dom kullanabilmek için
  • en dışa: Router (BrowserRouter) etiketiyle yazılır.
  • re-render edilecek kısmın linki
    Link etiketiyle yazılır. Örn:
    <Link to="/">Home</Link>
  • render edilecek alan Routes etiketinin altına yazılır. (Daha önceki versiyonda Switch kullanılıyor.) Burada koşul belirtilir. Uygun koşula ait kısım render edilir.
  • Routes içine koşullar Route etiketi ile yazılır. Örn:
    <Route path="/about" element={<About/>}/>
    Bu örnekte çalıştırılan path "/about" ise <About/> kompanenti render edilir.

Örnek App.js:

import "./App.css";
import { Routes, Route, Link, BrowserRouter as Router } from "react-router-dom";

function App() {
  return (
    <>
      <Router>
        <Link to="/">Home</Link> <br />
        <Link to="/about">About</Link>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Router>
    </>
  );
};

function Home() {
  return (
    <>
      <main>
        <h2>Welcome to the homepage!</h2>
        <p>You can do this, I believe in you.</p>
      </main>
      <nav>
        <Link to="/about">About</Link>
      </nav>
    </>
  );
};

function About() {
  return (
    <>
      <main>
        <h2>Who are we?</h2>
        <p>That feels like an existential question, dont you think?</p>
      </main>
      <nav>
        <Link to="/">Home</Link>
      </nav>
    </>
  );
};

export default App;

                

Exact Prop

v6 için gerekli değil.

v5 te Switch etiketi içinde yazılan path yukarıdan aşağı doğru taranır. İlk eşleşmede değeri getirir. Bu nedenle path="/" üstteyse path="/..." olanlara geçmez. bunu engellemek için exact probu kullanılır.

URL Parameters

Bütün fonksiyonlar companent/Rooting adında kendi .js dosyalarına taşındı ve App.js içine import edildi.

User.js kompanenti oluşturuldu ve import edildi.

App.js Routes alanına ilgili route yazıldı.

<Route path="/users/:id" element={<User />} />
              
bu sayede id değişkeni olan durumlarda User kompanenti kullanılacak. Burayda id yerine yazılan değer de User kompanentinde yakalanacak.

Users içinde fake api den alınan bilgi kullanıldı ve tekil User sayfaları için link oluşturuldu.

import axios from "axios"; 
import { useEffect, useState } from "react"; 
import { Link } from "react-router-dom";  

function Users() { 
  const [users, setUsers] = useState([]); 
  const [loading, setLoading] = useState(true); // Loading... yazısı için 

  useEffect(() => { 
    axios("https://jsonplaceholder.typicode.com/users") 
      .then((res) => setUsers(res.data)) 
      .catch((e) => console.log(e)) 
      .finally(() => setLoading(false)); 
  }, []); 
  return ( 
    <div> 
      <h1>Users</h1> 
      {loading && <div>Loading...</div>} 
      <ul> 
        {users.map((user) => ( 
          <li key={user.id}> 
            <Link to={`/users/${user.id}`}>{user.name}</Link> {/* user.id params olarak gönderildi. User tekil sayfasında tutulup kullanılacak. */}
          </li> 
        ))} 
      </ul> 
    </div> 
  );
} 

export default Users;

              

user içinde :id parametresi yakalandı ve axios içinde ilgili veriyi çekmek için kullanıldı.

Her id değiştiğinde sayfanın useEffect çalıştırması için dependency array içine id eklendi.

User.js

import { useParams } from "react-router-dom"; 
import { useState, useEffect } from "react"; 
import axios from "axios"; 
import Users from "./Users"; 
import { Link } from "react-router-dom"; 

function User() { 
  const [user, setUser] = useState({}); 
  const [loading, setLoading] = useState(true); 

  const { id } = useParams(); // Bağlantıdan gelen params yakalandı. 

  useEffect(() => { 
    axios(`https://jsonplaceholder.typicode.com/users/${id}`) 
      .then((res) => setUser(res.data)) 
      .catch((e) => console.log(e)) 
      .finally(() => setLoading(false)); 
  }, [id]);  //  dependency array alanına id girildi ki bu işlem her id değiştiğinde yenilensin.  
  return ( 
    <div> 
      {loading && <div>Loading...</div>} 
      <h1>User Detail</h1> 
      <p>Name: {user.name}</p> 
      <p>Email: {user.email}</p> 
      {!loading && <code>{JSON.stringify(user)}</code>} 
      <p>id: {id}</p> 
      <Link to={`/users/${parseInt(id) + 1}`}>  //  toplama işleminin yapılabilmesi için string yapıdaki id integer olarak değiştirildi. 
        Next User ({parseInt(id) + 1}) 
      </Link> 
    </div> 
  );
} 

export default User;

              

Nesting

Mevcut kompanent açık kalırken ona bağlı başka bir kompanentin yüklenmesi işlemi.

v6 ya göre yapımı için: tıklayın

İlk önce App.js içindeki Routes alanında child olan kompanenti ayarlıyoruz.

<Routes>
  <Route path="/about" element={<About/>}/> 
  <Route path="/" element={<Home />} /> 
  <Route path="/users/*" element={<Users/>}> 
    <Route path=":id" element={<User />} /> // bu kısım child kompanente ait. path kısmına da parent kompanentin devamına gelecek kısım yazılır. 
  </Route> 
</Routes> 
              

Daha sonra Users.js içine Outlet fonksiyonu "react-router-dom" üzerinden import edilir.

import { Link, Outlet} from "react-router-dom";
              

Child kompanentin olmasını istediğimiz yere kompanent yazar gibi <Outlet/> eklenir.

  ... 
  return( 
  ... 
  <Outlet/> 
  ...
  )

No Match (404)

Varolan sayfaların dışında kalan tüm path değişkenleri için App.js Routes içine:

<Route path="*" element={<Error404 />} />
              
eklenir. Error404 adlı kompanent yaratılır ve App.js içine import edilir.

Formik Kurulum

Yeni bir react projesi oluşturduk.

npx create-react-app formik
              

formik paketi projeye dahil edilir.

npm i formik
              

formik dökümantasyonu için tıklayınız

App.js içinde kullanılır.

import React from 'react'; 
import { Formik, Field, Form } from 'formik';
ile formik bileşenleri import edilir.
import './App.css'; 

function App() { 
  return ( 
    <div className="App"> 
      <h1>Sign Up</h1> 
    <Formik // Formik yapıları bu etiketin arasına kurgulanır. Açılış etiketine de gerekli formüller yazılır. 
      initialValues={{ // Bu alana state yapısındaki gibi alınacak verinin boş halini giriyoruz. 
        firstName: '', 
        lastName: '', 
        email: '', 
      }} 
      
      onSubmit={(values) => { // input edilen değerlere ne yapılacağını belirleyen fonksiyon 
        console.log(values); 
      }}
    > 
      
      <Form> // Formik yapısının Form oluşturma etiketi. 
        <label htmlFor="firstName">First Name</label> 
        <Field id="firstName" name="firstName" placeholder="Jane" /> // Field bize inpup yaratıyor. Field alanındaki name ile initialValues alanındaki key aynı olmalı. id önemli değil. 
        <br />
        <br /> 

        <label htmlFor="lastName">Last Name</label> 
        <Field id="lastName" name="lastName" placeholder="Doe" /> 
        <br />
        <br /> 

        <label htmlFor="email">Email</label> 
        <Field
          id="email"
          name="email"
          placeholder="jane@acme.com"
          type="email"
        /> 
        <br />
        <br /> 
        <button type="submit">Submit</button> 
      </Form> 
    </Formik> 
    </div> 
  );
} 

export default App;

              

handleSubmit & handleChange

Formik yapısındaki Form - Field yapısı yerine html yapısında kullanılan form-input yapısını kullanamabilmek için kullanılır.

Bir önceki sayfada yazılan kodun handleSubmit ve handleChange ile düzenlenmişi:

import React from 'react'; 
import { Formik } from 'formik'; // Form ve Field kısnmına gerek kalmadı. 
import './App.css'; 

function App() { 
  return (br
    <div className="App"> 
      <h1>Sign Up</h1> 
    <Formik 
      initialValues={{ 
        firstName: '', 
        lastName: '', 
        email: '', 
      }} 
      
      onSubmit={(values) => { 
        console.log(values); 
      }} 
    > 
      {({handleSubmit, handleChange }) => ( // parametre olarak handleSubmit, handleChange kullanılan bir fonksiyon return edilir. Bu iki fonksiyon Formik modülünde tanımlıdır. 
        <form onSubmit={handleSubmit}> // handleSubmit fonksiyonu onSubmit işlemine atanır. 
        <label htmlFor="firstName">First Name</label> 
        <input name="firstName" onChange={handleChange}/>  // handleChange fonksiyonu onChange işlemine atanır. 
      
        <br />
        <br /> 

        <label htmlFor="lastName">Last Name</label> 
        <input name="lastName" onChange={handleChange}/> 
        
        <br />
        <br /> 

        <label htmlFor="email">Email</label> 
        <input name="email" type="email" onChange={handleChange}/> 
        <br />
        <br /> 
        <button type="submit">Submit</button> 
      </form> 
      )} 
      
    </Formik> 
    </div> 
  ); 
} 

export default App;
              

Radio / Checkbox / Dropdown

forma eklenecek bilgilerin alınabilmesi için formda name keyine karşılık gelen value initialValues alanına eklenmeli.

... 
<Formik 
  initialValues={{ 
    firstName: "", 
    lastName: "", 
    email: "drmuratgokduman@gmail.com", // formda varsayılan olarak gelmesini istediğimiz değer varsa bu şekilde yazabiliriz. bunu da value={values.email} ile formda yakalayabiliriz. 

    gender: "male", 
    hobies: [], 
    county: "tr", 
  }} 
  ... 

Formda yaptığımız değişiklerin etkisini görmek için:

... 
          <code>{JSON.stringify(values)}</code> 
    </form> 
  )} 
</Formik> 
... 
              

Radio

... 
<span>Male</span> 
<input 
  type={"radio"} 
  name="gender"  // initialValues alanı ile eşleşecek. 
  value={"male"} // seçilirse forma gönderilecek veri 
  onChange={handleChange} 
  checked={values.gender === "male"} // form varsayılanı olarak initialValues içinde girilen değerin form açıldığında seçili gelmesini sağlar. 
/> 
<span>Female</span> 
<input 
  type={"radio"} 
  name="gender" 
  value={"female"} 
  onChange={handleChange} 
  checked={values.gender === "female"} 
  ... 
              

Checkbox

<div> 
  <input 
    type="checkbox" 
    name="hobies" 
    value="Play Playstation" 
    onChange={handleChange} 
  /> 
  Play Playstation 
</div> 
<div> 
  <input 
    type="checkbox" 
    name="hobies" 
    value="Read a Book" 
    onChange={handleChange} 
  /> 
  Read a Book 
</div> 
<div> 
  <input 
    type="checkbox" 
    name="hobies" 
    value="Write Code" 
    onChange={handleChange} 
  /> 
  Write Code 
</div>
              

Dropdown

<select name="coutry" value={values.county} onChange={handleChange}> // value={values.county} ile formda değişiklik yoksa default değer kullanılır. 
  <option value="tr">Turkey</option> 
  <option value="en">England</option> 
  <option value="usa">USA</option> 
</select>
              

useFormik

Formu <Formik> etiketi ile sarmalamadan formik yapısı kullanmak için useFormik hooku kullanılabilir.

returndan önceki tanımlama kısmına

const Formik = useFormik({ 
  initialValues: { 
    firstName: "", 
    ... 
  } 
  onSubmit: (values) => { 
    console.log(values); 
  }, 
})
              
yazılır. form içinde kullanılan diğer hooklar
handleSubmit: Formik.handleSubmit
handleChange: Formik.handleChange
gibi kullanılabilir.

veya tanımda Formik yerine yazılarak daha önce kullanıldıkları halleri ile kullanılabilirler.

                const { handleSubmit, handleChange, values } = useFormik({ ...
              

Daha önceki konularda oluşturduğumuz formun useFormik ile yazılmış hali:

import React from "react"; 
import { useFormik } from "formik"; //ile useFormik import edilir. 
import "./App.css"; 

function App() { 
  const { handleSubmit, handleChange, values } = useFormik({ // ile tanımlamalar yapılır. 
    initialValues: { 
      firstName: "", 
      lastName: "", 
      email: "drmuratgokduman@gmail.com", 
      gender: "male",  
      hobies: [], 
      county: "tr", 
    }, 
    onSubmit: (values) => { 
      console.log(values); 
    }, 
  }); 

  return ( 
    <div className="App"> // Form <Formik> etiketi kullanılmadan yazılır. 
      </div> 
      <h1>Sign Up</h1> 

      <form onSubmit={handleSubmit}> 
        <label htmlFor="firstName">First Name</label> 
        <input name="firstName" onChange={handleChange} /> 

        <br /> 
        <br /> 

        <label htmlFor="lastName">Last Name</label> 
        <input name="lastName" onChange={handleChange} /> 

        <br />
        <br /> 

        <label htmlFor="email">Email</label> 
        <input
          name="email"
          type="email"
          value={values.email}
          onChange={handleChange}
        /> 
        <br />
        <br /> 

        <span>Male</span> 
        <input
          type={"radio"}
          name="gender"
          value={"male"}
          onChange={handleChange}
          checked={values.gender === "male"}
        /> 
        <span>Female</span> 
        <input
          type={"radio"}
          name="gender"
          value={"female"}
          onChange={handleChange}
          checked={values.gender === "female"}
        /> 
        <br />
        <br /> 

        <div> 
          <input
            type="checkbox"
            name="hobies"
            value="Play Playstation"
            onChange={handleChange}
          /> 
          Play Playstation 
        </div> 
        <div> 
          <input
            type="checkbox"
            name="hobies"
            value="Read a Book"
            onChange={handleChange}
          /> 
          Read a Book 
        </div> 
        <div> 
          <input
            type="checkbox"
            name="hobies"
            value="Write Code"
            onChange={handleChange}
          /> 
          Write Code 
        </div> 

        <br />
        <br /> 

        <select name="coutry" value={values.county} onChange={handleChange}> 
          <option value="tr">Turkey</option> 
          <option value="en">England</option> 
          <option value="usa">USA</option> 
        </select> 
        <br />
        <br /> 
        <button type="submit">Submit</button> 
        <br />
        <br /> 
        <code>{JSON.stringify(values)}</code> 
      </form> 
    </div> 
  );
} 

export default App;

              

Form Validasyonları (yup)

Validasyon çalışması için formumuzu email, password ve confirm password imputlarıyla ouşturduk.

Validasyon işlemini yup paketi ile yapacağız. Bunun için terminale:

npm i yup
              

App.js içindeki kompanent src/components/Singup.js içine taşındı ve App.js içinde import edilerek kullanıldı.

src/components/validations içinde validasyon işlemi tanımlandı ve export edildi.

import * as yup from "yup"; 

const validations = yup.object().shape({ // validasyon kuralları yup içinde obje olarak tanımlanır. email: // buradaki key ler valide edilecek formun initialValues alanındakiler ile aynı olmalı. yup.string() // string yapıda .email() // email formatında .required(), // ve zorunlu olarak doldurulacak. password: yup.string() .min(5) // en az 5 karakter. .required(), passwordComfirm: yup.string() .oneOf([yup.ref("password")]) // password alanındaki veriyle aynı olmalı. .required(), }); export default validations;

validasyon kullanılacağı Singup.js içine import edilir ve formik yapısı içindeki validationSchema ile eşleştirilir.

import React from "react"; 
import { useFormik } from "formik"; 
import validations from "./validation"; 

function Singup() { 
  const { handleSubmit, handleChange, values } = useFormik({ 
    initialValues: { 
      email: "", 
      password: "", 
      passwordComfirm: "", 
    }, 
    onSubmit: (values) => { 
      console.log(values); 
    }, 
    validationSchema: validations // validations import edilirken validationSchema adıyla alınırsa bu eşleştirmeye gerek kalmadan sadece "validationSchema" yazılarak da kullanılabilir. 
  }); 
  return ( 
    <div> 
      <h1>Sign Up</h1> 
      <form onSubmit={handleSubmit}> 
        <label>Email</label> 
        <input
          name="email"
          value={values.email}
          onChange={handleChange}
        />
        <br />
        <br /> 
        <label>Password</label> 
        <input
          name="password"
          value={values.password}
          onChange={handleChange}
        />
        <br />
        <br /> 
        <label>Password Comfirm</label> 
        <input 
          name="passwordComfirm"
          value={values.passwordComfirm}
          onChange={handleChange}
        /> 
        <br />
        <br /> 
        <button type="submit">Submit</button>
        <br />
        <br /> 
        <code>{JSON.stringify(values)}</code> 
      </form> 
    </div> 
  );
} 

export default Singup;

              

Hata Mesajlarının Gösterimi

Bunun için formik yapısına errors, touched ve handleBlur özellikleri import edilir.
errors: hata mesajlarını yakalar. hatanın olduğu kısım key, hata mesajı value olan bir object döner.
touched: yazıldığı kısma kullanıcı teması olma durumunu verir.
handleBlur: touched değerini günceller.

Koşul cümlesi ile hata mesajı alınır.

{errors.email && touched.email && <div className="error"><br/>{errors.email}</div>}
              

errors.email: hata mesajı varsa
touched.email: email inputuna dokunulduysa
<div className="error"><br/>{errors.email}</div>} kodunu yerleştir.

Örnek:

  ... 
return( 
  ... 
  <input
    name="password"
    value={values.password}
    onChange={handleChange}
    onBlur={handleBlur}
  /> 
  <br /> 
  {errors.password && touched.password && <div className="error"><br/>{errors.password}</div>} 
  <br /> 
  ... 
)
              

Koşula göre verilen hata kodu default haliyle ingilizce açıklamalar verir. Bunu özelleştirebiliriz. Bunun için validation.js içinde:

import * as yup from "yup"; 

const validations = yup.object({ 
  email: 
    yup.string() 
    .email('Geçerli bir email girin') 
    .required('Doldurulması zorunludur'), 
  password: 
    yup.string() 
    .min(5, 'Parolanız en az 5 karakter olmalıdır') 
    .required('Doldurulması zorunludur'), 
  passwordComfirm: 
    yup.string() 
    .oneOf([yup.ref("password")], 'Parolalar uyuşmuyor') 
    .required('Doldurulması zorunludur'), 
}); 

export default validations;

              

React.memo

React içindeki gereksiz render işlemlerini engelleyip performansı arttırmak için kullanılır.

React projesinde bir kompanentin içinde yer alan kompanent dış kompanent her render edildiğinde kendisinde bir değişiklik olmasa bile yeniden render ediliyor. Bunu engellemek için export işlemi sırasında React.memo kullanılır. Örn:

export default React.memo(Header)
              

Bu durumda Header kompanentinde veya gelen propunda bir değişiklik olmadıkça re-render edilmez.

Örnek

Header.js:

import React from 'react' 

function Header({number}) { 
    console.log("Header Component Re-Rendered!"); // Re-render işlemini konsoldan takip edebilmek için yazıldı. 
  return ( 
    <div>Header - {number}</div> 
  ) 
} 

export default React.memo(Header) // export edilecek fonksiyon React.memo ile sarmalandı.
              

App.js

import './App.css'; 
import { useState } from "react"; 
import Header from './components/Header' 

function App() { 
  const [number, setNumber] = useState(0) 
  return ( 
    <div className="App"> 
      <Header number={number < 5 ? 0 : number} /> // Header içine gönderilecek number probu number 5 ten küçük olduğu sürece 0 olarak gönderilecek. 
      <hr /> 
      <h1>{number}</h1> 
      <button onClick={()=>setNumber(number +1)}>Click</button> 

    </div> 
  );
} 

export default App;

              

Yukarıdaki örnekte number stateti 0 dan başlar ve her butona basıldığında number 1 artar. number değeri 5i geçene kadar gönderilen number propu 0 olarak kalır. Bu nedenle de o ana kadar Header kompanentinde re-render olmaz.

Header içinde proptan gelen veri kullanılmasa da değer değiştiğinde re-rendering olur.

useMemo

Aynı içeriğe sahip object veya array yapılarının denkliği js üzerinden sorgulandığında, arka plandaki referansları farklı olduğundan değer false döner. Bu nedenle prop olarak gönderilen object veya array kompanent içindeyse, içinde olduğu kompanent her render edildiğinde prop da yeni veri göndermiş gibi davranır.

Bundan kurtulmak için prop olarak gönderilecek object veya array kompanentin dışında tanımlanabilir.

Kompanentin içinde yazılması gerekiyorsa useMemo hooku kullanılır. örn:

const data = useMemo(()=>{ 
  return {name: "Murat"} 
},[])
              

useMemo useEffect gibi davranır. dependence array'e yazılan değer değiştiğinde içerideki değeri tekrar döndürür.

Örnek 1

Header.js:

import React from 'react' 

function Header({number, data}) { 
    console.log("Header Component Re-Rendered!"); 
  return ( 
    <div>Header - {number} - {JSON.stringify(data)</div> 
  ) 
} 

export default React.memo(Header) //export edilecek fonksiyon React.memo ile sarmalandı.
              

App.js

import './App.css'; 
import { useState } from "react"; 
import Header from './components/Header' 

function App() { 
  const [number, setNumber] = useState(0) 
  const data = useMemo(()=>{ // data object yapısı useMemo ile tanımlandı. 
    return {name: "Murat"}
  },[]) // [] içine girilen yapı değiştiğinde verinin yeniden gönderimini yapar. [] içi boşsa değeri sabit tutar. 
  return ( 
    <div className="App"> 
      <Header number={number < 5 ? 0 : number} />
      <hr /> 
      <h1>{number}</h1> 
      <button onClick={()=>setNumber(number +1)}>Click</button> 

    </div> 
  );
} 

export default App;
              

Örnek 2

Bize değişken olarak verilen değer bir fonksiyonun çıktısıysa ve biz her seferinde bu fonksiyonun çalışmasını istemiyorsak useMemo kullanırız.

App.js:

import './App.css'; 
import { useMemo, useState } from "react"; 
import Header from './components/Header' 

function App() { 
  const [number, setNumber] = useState(0) 
  const [text, setText] = useState("") 
  
  const data = useMemo(()=>{ 
    return caculateObject() 
  },[]) 

  return ( 
    <div className="App"> 
      <Header number={number < 5 ? 0 : number} data={data} /> 
      <hr /> 
      <h1>{number}</h1> 
      <button onClick={()=>setNumber(number +1)}>Click</button> 
      <br /> 
      <br /> 
      <input value={text} onChange={({target}) => setText(target.value)}/> 
    </div> 
  ); 

  function caculateObject(){ // Objeyi return edecek fonksiyon. 
    console.log("Calculating..."); 
    for(let i=0; i<1000000000; i++){} // hesaplamanın zaman aldığı bir işlem simüle edildi. 
    console.log("Calculating completed!"); 

    return {name: "Murat"} 
  } 

} 

export default App;

              

Biz burada useMemo kullanmak yerine

const data = caculateObject()
ile tanımlama yaparsak forma her bir harf girmeye çalıştığımızda yukarıdaki fonksiyon tekrar çalışır ve bizi bekletir.

useCallback

İç içe kompanent yapısında içteki kompanente prop olarak fonksiyon gönderdiğimizde ve dıştaki kompanent re-render edildiğinde, fonksiyon baştan hesaplandığı için React.memo kullanılsa bile içteki kompanent de re-render ediliyor. Bunu engellemek için useCallback kullanılıyor.

Kullanımı useMemo gibi. Fonksiyon useCallback içinde tanımlanır ve prop olarak gönderilir.

const increment = useCallback(() => { 
  setNumber(number + 1) 
},[number]) 
              
setNumber(number + 1) fonksiyonunun döndüğü durumda [] içine number yazılmazsa ilk değeri 0 olan number fonksiyonda işlenir ve sonuca sabitlenir. Number güncellendiğinde fonksiyonun tekrar çalışabilmesi için dependence array ("[]") içine yazılır. Ancak bu durmumda da yeni bir fonksiyon tanımlandığından Header re-render edilir. Bunu engellemek için fonksiyon number olmadan yazılmalıdır.

const increment = useCallback(() => { 
  setNumber((preState) => preState + 1); 
}, []); 
              

Örnek

App.js

import "./App.css"; 
import { useMemo, useState, useCallback } from "react"; 
import Header from "./components/Header"; 

function App() { 
  const [number, setNumber] = useState(0); 
  const [text, setText] = useState(""); 

  const increment = useCallback(() => { // Fonksiyon useCallback içinde tanımlanır. 
    setNumber((preState) => preState + 1); 
  }, []); 

  return ( 
    <div className="App"> 
      <Header increment={increment} /> // tanımlanan fonksiyon prop ile gönderilir. 
      <hr /> 
      <h1>{number}</h1> 

      <br /> 
      <br /> 
      <input value={text} onChange={({ target }) => setText(target.value)} /> 
    </div> 
  );
} 

export default App;

              

Header.js:

import React from 'react' 

function Header({number, increment}) {  // ile prop alındı. 
    console.log("Header Component Re-Rendered!"); 
  return ( 
    <div> 
        Header - {number} 
        <br /> 
        <br /> 
        <button onClick={increment}>Click</button> {/* ile alınan prop kullanıldı. */}
    </div> 
  )
} 

export default React.memo(Header)
              

Context Nedir?

Elimizdeki datanın tüm kompanentlerde kullanılabilmesini sağlar. Contex içindeki dataya herhangi bir kompanentten ulaşıp manipule edebiliriz.

Context Oluşturmak

src/context klasörü içine ThemeContext.js dosyası oluşturulur.

import { createContext } from "react";

const ThemeContext = createContext(); // ile context yaratıldı

export default ThemeContext; 
              

Context içine veri göndermek için App.js:

import './App.css'; 
import Button from './components/Button'; 
import ThemeContext from "./context/ThemeContext"; // ile context import edildi.

function App() { 
  return ( 
    <ThemeContext.Provider value="dark"> // ile içine yazılacak tüm kompanentlere veri gönderildi.
      <Button/> 
    </ThemeContext.Provider> 
  );
} 

export default App; 

              

Context içindeki veriyi almak için Button.js içine:

import { useContext } from 'react' 
import ThemeContext from "../context/ThemeContext"; 

function Button() { 
    const data = useContext(ThemeContext) // data ile ThemeContext içinde gönderilen veri değişkene atandı.
    console.log(data); 
  return ( 
    <div>Button ({data})</div> 
  ) 
} 

export default Button
              

Context Provider

Children: bir kompanenti kapalı parantezle değil de html tagi gibi yazarsak arasına yazdığımız değerler prop gibi kompanente gönderilir ve chidren ile yakalanıp kullanılabilir.

chidren perspektifinde ThemeContext.Provider etiketi ve ona eklenen veriler ThemeContext.js dosyasına aktarılabilir.

ThemeContext.js:

import { createContext, useState } from "react"; 

const ThemeContext = createContext(); 

export const ThemeProvider = ({ children }) => { //  ThemeProvider değişkeni kompanent yapısıyla export edilir. İçine yazılacaklar children ile prop olarak alınır.
    const [theme, setTheme] = useState("dark")  // ile state oluşturuldu. 
    const values = { 
        theme, 
        setTheme 
    }  //  ile state değerleri değişkene atandı. 
  return <ThemeContext.Provider value={values}>{children}</ThemeContext.Provider>;  {/*  ThemeContext.Provider parantezleri, values verisi ve children verisi return edilir. */}
}; 

export default ThemeContext;

              

App.js içinde:

import './App.css'; 
import Button from './components/Button'; 
import Header from './components/Header'; 
import {ThemeProvider} from "./context/ThemeContext"; 

function App() { 
  return ( 
    <ThemeProvider> // ile kompanent içinde children olacak şekilde kurgulanır. Buradan gönderilen veri children olarak ThemeContext.js içinde kullanılır ve o perspektifte render edilir.
      <Header/> 
      <hr /> 
      <Button/> // chidren olarak Header ve Button kompanentleri yerleştirilir.
    </ThemeProvider> 
  );
} 

export default App;
              

Kompanent içinden context içindeki veriyi almak ve manipule etmek için Header.js

import React, { useContext } from 'react' 
import ThemeContext from '../context/ThemeContext' 

function Header() { 
    const {theme, setTheme} = useContext(ThemeContext) // ile ThemeContext içinden gönderilen value yakalandı.

  return ( 
    <div>Header: {theme} <button onClick={()=>setTheme(theme === "dark" ? "light" : "dark")}>Click</button> 
    </div> 
  )
} 

export default Header 
              

Aynı işlem Button.js içinde de tekrarlanabilir. Her iki buton da theme değerini değiştirir.

import { useContext } from 'react' 
import ThemeContext from "../context/ThemeContext"; 

function Button() { 
    const {theme, setTheme} = useContext(ThemeContext) 
  return ( 
    <div> 
      Active Theme: {theme} 
      <button onClick={()=>setTheme(theme === "dark" ? "light": "dark")}>Click</button> 
    </div>  
  )
} 

export default Button 
              

Theme Switcher Yapımı

Kapsayıcı kompanent olması için Container kompanenti oluşturuldu ve diğer kompanenetler onun içinde kullanıldı. Container kompanenti de App.js içinde import edilip kullanıldı.

Daha önce ThemeContext içinde gönderdiğimiz veriyi Container içindeki kapsayıcı div etiketine className vermek için kullandık. Bu className değerini de App.css içinde style ile karşıladık.

App.js

import './App.css'; 
import Container from './components/Container'; 
import {ThemeProvider} from "./context/ThemeContext"; 

function App() { 
  return ( 
    <ThemeProvider> 
      <Container/> 
    </ThemeProvider> 
  ); 
} 

export default App;
 
              

Container.js

import React, { useContext } from "react"; 
import Button from "./Button"; 
import Header from "./Header"; 
import ThemeContext from "../context/ThemeContext"; 

function Container() { 
  const { theme } = useContext(ThemeContext); 
  return ( 
    <div className={`app ${theme}`}> // butona her basıldığında theme değiştiğinden div etiketinin aldığı className de değişiyor. 
      <Header /> 
      <hr /> 
      <Button /> 
    </div> 
  );
} 

export default Container;

              

App.css

.app { 
  text-align: center; 
  height: 100vh; 
} 

.dark { 
  color: white; 
  background-color: black; 
}
              

Context Provider Side Effects

theme bilgisini localStorage üzerine ekleyeceğiz. Bu sayede sayfa yenilendiğinde son verdiğimiz hali bize gösterecek.

ThemeContext.js içinde:

import { createContext, useEffect, useState } from "react"; 

const ThemeContext = createContext(); 

export const ThemeProvider = ({ children }) => { 
  const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light'); // localStorage içinde theme keyine ait value varsa getir. yoksa 'light' değerini ata.

  useEffect(()=>{ 
    localStorage.setItem("theme", theme) 
  },[theme]) // theme değeri her değiştiğinde yeni değeri localStorage içine theme keyine karşılık ata.

  const values = { 
    theme, 
    setTheme, 
  }; 

  return ( 
    <ThemeContext.Provider value={values}>{children}</ThemeContext.Provider> 
  ); 
}; 

export default ThemeContext;

              

Multi Context

Yeni bir context oluşturduk. UserContext.js:

const { createContext, useState } = require("react"); 

const UserContext = createContext() 

export const UserProvider = ({children})=> { // App.js içinde bu contexten gelen veriyi alacak olan kompanentleri sarmalaması için UserProvider tanımlanır.
    const [user, setUser] = useState(null) // context ile göndermek için state oluşturuldu.
    const values = {
        user,
        setUser,
    } 

    return <UserContext.Provider value={values}>{children}</UserContext.Provider> 
} 

export default UserContext
              

App.js içinde

import './App.css'; 
import Container from './components/Container'; 
import {ThemeProvider} from "./context/ThemeContext"; 
import { UserProvider } from './context/UserContext'; 

function App() { 
  return ( 
    <ThemeProvider> 
      <UserProvider> // ile Container sarılır.
        <Container/> 
      </UserProvider> 
    </ThemeProvider> 
  );
} 

export default App;
 
              

Profile.js kompanenti oluşturulur ve Container içine import edilir.

Profile.js içinde:

import { useContext, useState } from "react"; 

import UserContext from "../context/UserContext";

function Profile() { 
  const { user, setUser } = useContext(UserContext); 
  const [loading, setLoading] = useState(false);  

  const handleLogin = () => { 
    setLoading(true); 
    setTimeout(() => { 
      setUser({ 
        id: 1, 
        username: "arslanng", 
        bio: "lorem ipsum dolor", 
      }); 
      setLoading(false); 
    }, 1500); // ile süre alan bir işlem simüle edilerek loading yazısı ekranda gösterilmiştir.
  }; 
  return ( 
    <div> 
      {!user && ( 
        <button onClick={handleLogin}> 
          {loading ? "loading..." : "Login"} 
        </button> 
      )} 
      {JSON.stringify(user)} 

      {user && <button onClick={()=>setUser(null)}>Logout</button>} 

    </div> 
  ); 
} 

export default Profile;

              

Custom Context Hook

Birden fazla yerde yapılacak context ile ilgili işlemler context içinde tanımlanıp export edilerek kullanılabilir.
useContext(ThemeContext); örneğimizde birden fazla kompanentte kullanılıyor. Kodu sadeleştirmek için bu işlemi kompanentte değil context dosyasında yapabiliriz.

ThemeContext.js:

import { createContext, useContext, useEffect, useState } from "react"; 

const ThemeContext = createContext(); 

export const ThemeProvider = ({ children }) => { 
  const [theme, setTheme] = useState(localStorage.getItem("theme") || "light"); 

  useEffect(() => { 
    localStorage.setItem("theme", theme); 
  }, [theme]); 

  const values = { 
    theme, 
    setTheme, 
  }; 

  return ( 
    <ThemeContext.Provider value={values}>{children}</ThemeContext.Provider> 
  ); 
}; 

export const useTheme = () => useContext(ThemeContext); // ile işlem tanımlanıp export edildi.
              

Button.js içinde:

import { useTheme } from "../context/ThemeContext"; // ile useTheme import edildi.  

function Button() { 
  const { theme, setTheme } = useTheme(); // ile fonksiyon kullanıldı ve veriler elde edildi. 
  return ( 
    <div> 
      Active Theme: {theme}{" "} 
      <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}> 
        Click 
      </button> 
    </div> 
  ); 
} 

export default Button; 

              

Header ve Container için de aynı işlem tekrarlanır.

Context Ödev Notları

Verinin yansıtıldığı kompanent içine veri alınamazsa veya alınması zaman alırsa gösterilmesi için if koşulu ile bir loading ifadesi return edilir. Yoksa kod hata veriyor.

Arayüzün Hazırlanması

Projenin amacı, bir kullanıcı renk seçtiğinde bu seçimin tüm kullanıcıları anlık etkilemesi.

Hocanın daha önceden hazırladığı bir backend dosyası ile çalışacağız.

Bu backend socket.io kütüphanesini kullanıyor.

Backendde olması gereken paketleri yüklemek için terminal backendde iken terminale:

npm i
              
yazılır.

Terminale npm start yazılarak backend çalıştırılır.

paket yükleme işlemi client için de yapılır.

client tarafında Palette adında bir kompanent oluşturularak içine bir input [type="color"] ve bir buton ekledik. Bunu App.js içinde kullandık. App.css ile de stil tanımları atadık.

Socket Server'a Bağlanmak

Client tarafını server'a bağlamak için socket.io-client kullanılır. Terminale:

npm install socket.io-client
              

src/socketApi.js dosyası oluşturuldu. içine:

import io from "socket.io-client"; 

let socket; 

export const init = () => { // bizim socket server bağlantı fonksiyonumuz. 
    console.log("Sunucuya bağlanılıyor...") 
    socket = io('http://localhost:3001', { // backend tarafında belirlenen server ile bağlantı sağlar. 
        transports: ["websocket"] 
    }) 

    socket.on("connect", () => // bağlantı sağlandığında aşağıdaki fonksiyonu çalıştırır.  
    console.log("Sununucuya bağlandı") 
    ); 
}
              

App.js içinde bu fonksiyon karşılanır ve kullanılır.

import { useEffect } from 'react'; 
import './App.css'; 
import Palette from './components/Palette'; 
import { init } from './socketApi'; 

function App() { 
  useEffect(()=>{ 
    init() 
  },[]) 
  return ( 
    <div className="App"> 
      <Palette/> 
    </div> 
  );
} 

export default App;

              

Backend'e Veri İletmek

Veri iletmek için socketApi.js içine aşağıdaki fonksiyon eklenir:

export const send = (color) => { 
  socket.emit("newColor", color) 
}
              
emit metodu clientte isek backende backendde isek cliente veri gönderir. İki parametre alır. Hangi kanal? data ne?. Kanal bilgisi backendden alınır.

Bu fonksiyon Palette.js içinde butonda onClick eventinde kullanılır.

import React, { useState } from "react"; 
import { send } from "../socketApi"; 

function Palette() { 
  const [color, setColor] = useState('') // renk bilgisinin tutulduğu state.
  return ( 
    <div className="palette"> 
      <input type="color" value={color} onChange={(e)=>setColor(e.target.value)} /> 
      <button onClick={()=>send(color)}>Click</button> 
    </div> 
  );
} 

export default Palette;

              

Bir Kanala Abone Olmak

Socket io üzerinden veriyi bir kanal aracılığı ile alıyoruz. Aldığımız kanalın adı backend tarafında belirtiliyor.

Veriyi almak için SocketApi.js ye aşağıdaki fonksiyonu ekliyoruz.

export const subscribe = (cb) => { // cb parametresi ile fonksiyon olarak verilen parametre karşılanır.
  socket.on("receive", (color)=>{ 
      console.log(color); 
      cb(color) // ile karşılanan fonksiyona parametre geçilerek çalıştırılır.
  })
} 
              

Tanımlanan fonksiyon App.js içinde uygulanır.

import { useEffect, useState } from 'react'; 
import './App.css'; 
import Palette from './components/Palette'; 
import { init, subscribe } from './socketApi'; 

function App() { 
  const [activeColor, setActiveColor] = useState('#969696') // ile kanaldan çekilecek veri için state oluşturulur.  

  useEffect(()=>{ 
    init(); 
    subscribe((color)=>{ // ile ilgili stateti set edecek fonksiyon kanaldan veri çekecek fonksiyona parametre olarak atanır. 
      setActiveColor(color)  // kanaldan gelen veri set edilir. 
    }); 
  },[]) 
  return ( 
    <div className="App" style={{backgroundColor: activeColor}}> // ile kanaldan alınan veri background-color olarak kullanılır. 
      <Palette activeColor={activeColor}/> // ile veri Palette kompanenetine prop olarak gönderilir. 
    </div> 
  ); 
} 

export default App;

              

background-color değiştiğinde diğer clientlerde inputun da uyumlu olarak değişmesi için prop olarak gönderilen activeColor bilgisi Palette.js içinde input value olarak kullanılır.

Socket Io hakkında

Socket.IO, bir istemci ile bir sunucu arasında düşük gecikmeli, çift yönlü ve olay tabanlı iletişim sağlayan bir kitaplıktır.

WebSocket protokolünün üzerine inşa edilmiştir ve HTTP uzun yoklama veya otomatik yeniden bağlanmaya geri dönüş gibi ek garantiler sağlar.

Socket.IO ne değildir?

Socket.IO bir WebSocket uygulaması DEĞİLDİR.

Socket.IO gerçekten de mümkün olduğunda aktarım için WebSocket kullansa da, her pakete ek meta veriler ekler. Bu nedenle, bir WebSocket istemcisi bir Socket.IO sunucusuna başarılı bir şekilde bağlanamaz ve bir Socket.IO istemcisi de düz bir WebSocket sunucusuna bağlanamaz.

Websoket Nedir?

WebSocket API, bir kullanıcının tarayıcısı ve bir sunucu arasında iki yönlü etkileşimli bir iletişim oturumu açmasını mümkün kılan gelişmiş bir teknolojidir.

Websocket durum bilgisi olan bir protokoldür, yani istemci ve sunucu arasındaki bağlantı, taraflardan biri (istemci veya sunucu) tarafından sonlandırılıncaya kadar canlı kalır. İstemci ve sunucudan herhangi biri tarafından bağlantıyı kapattıktan sonra, bağlantı her iki uçtan da sonlandırılır.

Socket.IO, mobil uygulamalar için bir arka plan hizmetinde kullanılmak üzere tasarlanmamıştır.

Socket.IO kitaplığı, sunucuya açık bir TCP bağlantısı tutar ve bu, kullanıcılarınız için yüksek pil tüketimine neden olabilir. Lütfen bu kullanım durumu için FCM gibi özel bir mesajlaşma platformu kullanın.

Detay okuma için tıklayınız.

Chat App Giriş

Uygulamanın çalışması için redis uygulamasını yükleyeceğiz. Bu uygulamada gelen mesajları depolamak ve görüntülemek için kullanacağız.

redis kurabilmek için önce wsl (The Windows Subsystem for Linux) kurmamız gerekiyor. Alternatif olarak Microsoft Open Tech‘in 64 bit Windows sürümleri için portlamış olduğu bir Redis sürümü bulunmaktadır. Bunun için tıklayınız

redisin çalışması için terminale:

server-redis
yazıyoruz.

mennankose.com/windowsta-redis-kullanimi

Redis – Remote Dictionary Server (Uzak Sözlük Sunucusu); ilişkisel olmayan anahtar/değer veri tabanlarını ve önbellekleri uygulamak için yaygın olarak kullanılan açık kaynaklı bir bellek içi veri deposudur.

Projenin backend tarafı bize hazır verildi. redisin çalışması için bir takım ortam değişkenleri tanımlıyoruz. Bunun için backend dizinine .env dosyası oluşturduk.

REDIS_HOST=localhost // redisin çalışacağı host: bizim için localhost 
REDIS_PORT=6374 // redisin çalışacağı port: 6374 default değerdir. 
REDIS_PASS= // boş bırakıyoruz.
              

backend tarafında gereken node paketlerini yüklemek için terminalde backend açılır ve br:

npm i
yazılır.

Yazılım geliştirirken rahat etmek için projeye nodemon ilave edilebilir. Bu sayede her değişiklikten sonra backend kendini tekrar başlatabilir. terminale:

npm install --save-dev nodemon

Backend - db - client veri aktarımı için bu projede socket.io kullanılıyor.

Chat Context

client tarafında src/context/ChatContext.js oluşturuludu ve içinde

import { createContext, useState } from "react"; 

const ChatContext = createContext() 

export const ChatProvider = ({children}) => { 
  const [messages, setMessages] = useState([]) 
  const values = { 
      messages, 
      setMessages 
  } 
  return( 
      <ChatContext.Provider value={values}>{children}</ChatContext.Provider> 
  )
}  

export default ChatContext;
              

client/src/component içinde ChatForm.js ve ChatList.js taslak komponent olarak oluşturuldu ve Container.js komponentinde kullanıldı.

import React from 'react'
import ChatList from './ChatList'
import ChatForm from './ChatForm'

function Container() {
  return (
    <div>
        <ChatList/>
        <ChatForm/>
    </div>
  )
}

export default Container
              

client/App.js içinde hepsi birleştirildi:

import './App.css'; 
import Container from './components/Container';
import {ChatProvider} from './context/ChatContext';

function App() {
  return (
      <ChatProvider>
        <Container/>
      </ChatProvider>
  );
}

export default App;

              

Temel Bileşenlerin Geliştirilmesi

components içinde style.module.css oluşturuldu ve içine hazır olarak verilen css bilgisi eklendi.

Mesaj gelmesini simüle etmek için context/textContext.js messages stateine iki tane mesaj default olarak girildi.

const [messages, setMessages] = useState([ 
  { message: "Selam" },
  { message: "Naber" },
]);
              

ChatList.js içinde alınan mesaj yerleştirildi.

import React from "react"; 
import { useChat } from "../context/ChatContext";
import ChatItem from "./ChatItem";
import styles from "./styles.module.css";

function ChatList() {
  const { messages } = useChat();
  return (
    <div className={styles.chatlist}> 
      <div>
        {messages.map((item, key) => (
          <ChatItem key={key} item={item} />
        ))}
      </div>
    </div>
  );
}

export default ChatList;

              

Bu yerleştirme sırasında ChatItem kompanenti oluşturuldu ve içine prop olarak gönderilen veri ile kullanıldı.

import React from "react";
import styles from "./styles.module.css";

function ChatItem({ item }) {
  return <div className={styles.chatItem}>{item.message}</div>;
}

export default ChatItem;

              

Chatform.js içinde formdan gelen veriyi alacak bir state oluşturuldu. Forma her veri girdiğinde sayfanın yenilenmemesi için onSubmit için tanımlanan fonksiyona e.preventDefault(); kodu eklendi.

import React, { useState } from "react";

import styles from "./styles.module.css";

function ChatForm() {
  const [message, setMessage] = useState("");
  const handleSubmit = (e) => {
    e.preventDefault(); // sayfanın yenilenmesini engeller.
    console.log(message);
    setMessage(""); // mesajı sıfırlayarak submit sonrası formun temizlenmesini sağlar.
  };
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          className={styles.textInput}
          value={message}
          onChange={(e) => setMessage(e.target.value)}
        />
      </form>
    </div>
  );
}

export default ChatForm;

              

Daha görsel olması için stiller düzenlenir.

Backend Bağlantısı

Backend de bize hazır gelen socket-io serverı ile bağlanabilmemiz için client tarafında src/socketApi.js oluşturuldu ve içine:

import io from "socket.io-client"; 

let socket;

export const init = () => {
  console.log("Connecting...");
  socket = io("http://localhost:3000", { // backendimiz neredeyse onun urlsi girilir.
    transports: ["websocket"],
  });

  socket.on("connect", () => console.log("Connected")); // socket connect olduğunda çalışacak fonksiyon.
}; 

              

Yukarıda yazılan init fonksiyonu Container.js içinde karşılanır.

import React, { useEffect } from "react"; 
import ChatList from "./ChatList";
import ChatForm from "./ChatForm";

import { init } from "../socketApi";

function Container() {
  useEffect(() => {
    init();
  }, []);
  return (
    <div className="App">
      <ChatList />
      <ChatForm />
    </div>
  );
}

export default Container;

              

Mesajların İletilmesi

Stil tanımlarını rahat yapmak için ChatContext içinde messages statei için kullandığımız default mesajla silindi.

mesajı göndermek için socketApi.js içinde:

export const sendMessage = (message) => { 
  if(socket) socket.emit("new-message", message); 
} 
              
socket.emit() işleminde ilk parametre gönderilecek kanalı ikincisi gönderilecek olan veriyi belirtir.

ChatForm.js içinde setMessages statei çekilir. Yazılan mesajın messages değişkenine atanması için kullanılır.

import React, { useState } from "react"; 

import styles from "./styles.module.css"; 
import { sendMessage } from "../socketApi";
import { useChat } from "../context/ChatContext";

function ChatForm() {
  const { setMessages } = useChat();
  const [message, setMessage] = useState("");

  const handleSubmit = (e) => { 
    e.preventDefault(); 
    console.log(message); 

    setMessages((prevState) => [...prevState, { message, fromMe: true }]); // formdan gelen message değişkenini chatContext teki messages değişkenine ekler. forMe parametresi ile de bizden çıkan mesajları işaretler. 
    sendMessage(message);  // ile mesajı gönderir. formMe message değişkenine dahil olmadığı için beckende gönderilmez.
    setMessage(""); 
  };
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          className={styles.textInput}
          value={message}
          onChange={(e) => setMessage(e.target.value)}
        />
      </form>
    </div>
  );
}

export default ChatForm;

              

Alınan mesajları listelemek için socketApi.js içinde aşağıdaki fonksiyon yazıldı:

export const subscribeChat = (cb) => { 
  if(!socket) return;

  socket.on("receive-message", (message)=>{ // receive-message kanalı dinlenir. Oradan gelen mesaj bilgisi alınır. 
    console.log("Yeni mesaj var", message); 
    cb(message)  // fonksiyonun kullanıldığı yerde parametre olarak belirtilen fonksiyon cb() olarak çekilir ve içine parametre olarak message değişkeni kullanılır. 
  }) 
}
              

Container.js içinde subscribeChat() fonksiyonu kullanılır.

import React, { useEffect } from "react"; 
import ChatList from "./ChatList";
import ChatForm from "./ChatForm";
import { useChat } from "../context/ChatContext";

import { init, subscribeChat } from "../socketApi";

function Container() {
  const { setMessages } = useChat();

  useEffect(() => {
    init();

    subscribeChat((message) => {
      setMessages((prevState) => [...prevState, { message }]);
    }); // subscribeChat() fonksiyonu ile alınan veri messages değişkenine eklenir. Bu verilerde forMe değeri bulunmaz.
  }, []); 
  return (
    <div className="App">
      <ChatList />
      <ChatForm />
    </div>
  );
}

export default Container;

              

ChatItem.js içinde listeleme yaparken forMe varlığı sorgulanır. forMe varsa ilave className alır. Buna bağlı olarak da stili değişir. Bizim gönderdiklerimizde forMe olur. Dışarıdan gelenlerde olmaz.

import React from "react"; 
import styles from "./styles.module.css";

function ChatItem({ item }) {
  return <div className={`${styles.chatItem} ${item.fromMe ? styles.right : ""}`}>{item.message}</div>;
}

export default ChatItem;

              

Mesajların Listelenmesi

SocketApi.js içine listelenmiş mesajları alması için bir fonksiyon eklendi.

export const subscribeInitialMessages = (cb) => { 
  if (!socket) return; 

  socket.on("message-list", (messages) => { 
    console.log("Initial", messages); 
    cb(messages); 
  }); 
}; 
              
Mesajların gösterilmesi için kullanılan tekniğin aynısı kullanıldı. Sadece kanal adı değiştirildi.

Gelen veri Container.js içinde karşılandı.

import React, { useEffect } from "react"; 
import ChatList from "./ChatList";
import ChatForm from "./ChatForm";
import { useChat } from "../context/ChatContext";

import { init, subscribeChat, subscribeInitialMessages } from "../socketApi";

function Container() {
  const { setMessages } = useChat();

  useEffect(() => {
    init();

    subscribeInitialMessages((messages) => setMessages(messages)) // backend tarafından alınan mesaj bilgisi messages değişkeni olarak atandı.
    subscribeChat((message) => {
      setMessages((prevState) => [...prevState, { message }]);
    });
  }, []);
  return (
    <div className="App">
      <ChatList />
      <ChatForm />
    </div>
  );
}

export default Container;

              

Geçmiş mesajlar uygulamamız ilk açıldığında messages değişkenine atanıp listeleniyor. Geldiği yere forMe özelliği olmadan gönderildiğinden tüm mesajlar aynı görünüyor. user girişi olmadan bunu değiştirmenin bir yolu yok.

Feed Scroll

Bunun için react-scrollable-feed kütüphanesini kullanacağız.

Terminale:

npm i react-scrollable-feed

Chatlist.js içinde modülü import edip scrollun beslendiği alanı modülle sarmalayacağır.

import React from "react"; 
import { useChat } from "../context/ChatContext";
import ChatItem from "./ChatItem";
import styles from "./styles.module.css";
import ScrollableFeed from "react-scrollable-feed"

function ChatList() {
  const { messages } = useChat();
  return (
    <div className={styles.chatlist}>
      <ScrollableFeed>
        {messages.map((item, key) => (
          <ChatItem key={key} item={item} />
        ))}
      </ScrollableFeed>
    </div>
  );
}

export default ChatList;

              

React Intl - Kurulum

Lokalization işlemleri yani dil ile alakalı işlemleri gerçekleştirmek.

Kullanıcağımız kütüphane React Intl terminale:

npm i react-intl

App.js içinde import edilir ve kullanılır.

import "./App.css";

import { IntlProvider, FormattedMessage, FormattedNumber } from "react-intl"; // ile kullanılacak ksımlar import edilir. 
import { useState } from "react";

const messages = {  // ile kullanılacak veri oluşturulur. 
  "tr-TR": {
    title: "Merhaba Dünya",
    description: "3 yeni mesaj",
  },
  "en-US": {
    title: "Hello World",
    description: "3 new messages"
  }
  
};

function App() {
  const [lang, setLang] = useState("tr-TR")  // ile dil değişimi için state tanımlanır. 
  return ( 
    <div className="App"> 
      <IntlProvider messages={messages[lang]}> // ile react-intl kullanılacak alan kaplanır ve yayınlanacak mesajın verildiği obje messages keyi ile belirtilir. 
        <FormattedMessage id="title" />  // id içinde verilen değer messages objesindeki gösterilmek istenen key değeridir. 
        <p> 
        <FormattedMessage id="description" /> 
        </p>
        <br /> <br />
        <button onClick={()=>setLang("tr-TR")}>TR</button>
        <button onClick={()=>setLang("en-US")}>EN</button>
      </IntlProvider>
    </div>
  );
}

export default App;

              

Default Locale

Açılışta browser dili ile aynı dilin atanmasını ve daha sonra seçilen dilin sayfa yenilendiğinde kalmasını sağlayacağız.

navigator arabirimi, kullanıcı aracısının durumunu ve kimliğini temsil eder. Komut dosyalarının onu sorgulamasına ve bazı etkinlikleri yürütmek için kendilerini kaydettirmesine olanak tanır. İleri okuma için tıklayınız.

import "./App.css"; 

import { IntlProvider, FormattedMessage } from "react-intl";
import { useEffect, useState } from "react";

const messages = {
  "tr-TR": {
    title: "Merhaba Dünya",
    description: "3 yeni mesaj",
  },
  "en-US": {
    title: "Hello World",
    description: "3 new messages",
  },
};

function App() {
  const defaultLocale = localStorage.getItem("lang") || navigator.language; // localStorage içinde lang tanımı varsa onu alır. Yoksa browser default değerini alır. 
  const [lang, setLang] = useState(defaultLocale);

  useEffect(()=>{ 
    localStorage.setItem("lang", lang);
  }, [lang]) // lang değeri değiştiğinde değişen değeri localStorage içine gönderir. 
  return (
    <div className="App">
      <IntlProvider locale={lang} messages={messages[lang]}>
        <FormattedMessage id="title" />
        <p>
          <FormattedMessage id="description" />
        </p>
        <br /> <br />
        <button onClick={() => setLang("tr-TR")}>TR</button>
        <button onClick={() => setLang("en-US")}>EN</button>
      </IntlProvider>
    </div>
  );
}

export default App;

              

Parametre Geçmek

Kullanılacak veri içine FormattedMessage içinden parametre gönderebiliriz.

import "./App.css"; 

import { IntlProvider, FormattedMessage } from "react-intl";
import { useEffect, useState } from "react";

const messages = {
  "tr-TR": {
    title: "Merhaba Dünya",
    description: "{count} yeni mesaj",  // parametrenin alınıp kullanıldığı yer. 
  },
  "en-US": {
    title: "Hello World",
    description: "{count} new messages",  // parametrenin alınıp kullanıldığı yer. 
  },
};

function App() {
  const defaultLocale = localStorage.getItem("lang") || navigator.language;
  console.log(defaultLocale);
  const [lang, setLang] = useState(defaultLocale);

  useEffect(()=>{
    localStorage.setItem("lang", lang);
  }, [lang])
  return (
    <div className="App">
      <IntlProvider locale={lang} messages={messages[lang]}>
        <FormattedMessage id="title" />
        <p>
          <FormattedMessage id="description" values={{count: 5}}/> // Parametrenin gönderildiği yer. 
        </p>
        <br /> <br />
        <button onClick={() => setLang("tr-TR")}>TR</button>
        <button onClick={() => setLang("en-US")}>EN</button>
      </IntlProvider>
    </div>
  );
}

export default App;

              

Neden Test Yazarız?

Birden fazla kompanent yazdığımızda ve her birini yazdıktan hemen sonra test ettiğimizde, daha sonra yazdığımız bir kodun daha öncekini bozup bozmadığını bilemeyiz. Bunun için her işlemin sonunda hepsini test etmek gerekir.

Bu süreci otomatize etmek için test yazarız.

npx create-react-app project
ile yeni bir proje oluşturduğumuzda ilk test dosyamız src/App.test.js olarak hazır gelir. terminale
npm test
yazarak çalıştırılır.

Test dosyaları KompanentAdi.test.js olarak yazılır.

App.test.js

import { render, screen } from '@testing-library/react'; 
import App from './App'; //  test edilecek kompanent import edildi. 

test('renders learn react link', () => {
  render(<App />); // App kompanentini render ederken
  const linkElement = screen.getByText(/learn react/i); // Ekranda "learn react" yazısını arar. ve linkElement değişkenine atar.
  expect(linkElement).toBeInTheDocument(); // linkElement değişkenini döküman içinde olma durumunu değerlendirir. True dönerse test olumlu döner.
});

              

React Testing Library Örnek 1

src/components/Counter dosyası içinde:

index.js:

import React, { useState } from "react"; 

function Counter() { 
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <button onClick={() => setCount(count - 1)}>Decrease</button>
    </div>
  );
  }
  
export default Counter;

              

ile test edilecek kompanent yazıldı ve App.js içinde kullanıldı.

Aynı klasör içide Counter.test.js:

import { render, screen } from "@testing-library/react"; //  "render" kompanent render eder. "screen" DOM üzerindeki nesneyi yakalar. 

import userEvent from "@testing-library/user-event"; 
import { act } from "react-dom/test-utils"; 
import Counter from ".";

describe("Counter Test", ()=>{  // Her testte ortak olan işlemler için testler bu yapı içine alınır. 

    let increaseBtn, decreaseBtn, count // ile ortak kullanılacak değişkenler tanımlanır. 
    
    beforeEach(()=>{  //  Test ifadesi başlamadan önce gereken işlemler burada yazılabilir. Her testten önce çalışır. 
        render();  //  ile counter render edildi 
    
        count = screen.getByText("0");
        increaseBtn = screen.getByText("Increase");  //  ile içinde "Increase" yazan kompanent bulundu 
        decreaseBtn = screen.getByText("Decrease");  //  ile içinde "Decrease" yazan kompanent bulundu. 
        console.log("her testten önce çalışırım");

    })

    beforeAll(()=>{  //  Bu ifade beforeEach den farklı olarak her testten önce tekrar çalışma için değil test sırasında testlerden önce bir kere çalışma için kullanılır. 
        console.log("en başta bir kere çalışırım"); 
    }) 

    afterEach(()=>{ 
        console.log("Her testten sonra çalışırım"); 
    }) 

    afterAll(()=>{
        console.log("en sonda bir kere çalışırım");
    })

    it('increase btn', ()=>{  //  test satırı "it" (veya "test") ile başlar. hemen arkasından açıklaması gelir. sonra callback ile test yazılır. 
    
        act(()=>{
            userEvent.click(increaseBtn);  //  ile butona tıklandı 
        })
        expect(count).toHaveTextContent("1") //  ile butona tıklandığında beklenen aksiyon yazıldı. 
    }) 
    it('decrease btn', ()=>{
        
        act(()=>{
            userEvent.click(decreaseBtn);
        })
        expect(count).toHaveTextContent("-1")
    })
})

              

React Testing Library Örnek 2

src/components/Todo dosyası içinde:

index.js:

import React, { useState } from 'react' 
const defaultItems = [ // ile default değerler atanır.
    {
        name: "Item A",
    },
    {
        name: "Item B",
    },
    {
        name: "Item C",
    },
]

function Todo() {
    const [text, setText] = useState("");
    const [items, setItems] = useState(defaultItems);
    const addItem = () => {
        setItems((prevState) => [...prevState, {name: text}]);
        setText("")
    }
  return (
    <div>
        <label htmlFor='input'>Input</label>
        <input id="input" value={text} onChange={(e)=> setText(e.target.value)}/>
        <button onClick={addItem}>Add</button>
        <br /><br />
        {
            items.map((item, key)=>(
                <div key={key}>{item.name}</div>
            ))
        }
    </div>
  )
}

export default Todo
              

Todo.test.js içinde:

import { render, screen } from "@testing-library/react"; 
import userEvent from "@testing-library/user-event";
import { act } from "react-dom/test-utils";

import Todo from "."; //  ile test edilecek kompanent import edilir.

describe("Todo testleri", () => { 
  let button, input;
  beforeEach(() => {
    render(<Todo />);
    button = screen.getByText("Add");
    input = screen.getByLabelText("Input");
  }); //  ile testlerde gerekecek tanımlar hazırlanır. 

  test("Varsyılanları listele", () => {
    const items = screen.getAllByText(/Item/i); // Ekranda Item ile başlayan metinleri bul. 

    expect(items.length).toEqual(3); //  Sorgula: bulunan itemlerden oluşan array 3 elemanlı mı?  
  });

  test("input ve buton dökümanda bulunmalı", () => {
    expect(button).toBeInTheDocument();
    expect(input).toBeInTheDocument(); //  button ve input değişkenine tanımlanmış elemanlar sayfada var mı? 
  });

  test("inputa string girilip butona basılınca listeye eklemeli.",()=>{
    const name = "Murat" //  forma yazılacak değişken 
    act(()=>{
        userEvent.type(input, name);  //  input değişkenine atadığımız html varlığına name değişkenini yaz. 
        userEvent.click(button); //  ile butona tıkla 
    })

    expect(screen.getByText(name)).toBeInTheDocument() //  Sorgula: name değişkeni dökümanda var mı? 
  })
});

              

create-react-library

Yaptığımız kütüphaneyi npmjs.com üzerinden paylaşmayı öğreneceğiz.

Yazacağımız kütüphanenin adı unique olmak zorunda. Bunu da buradan kontrol edebiliriz.

terminale:

npx create-react-library

veya

npm install -g create-react-library
ile global kurulur ve
create-react-library
ile çalıştırılır.

Gelen form terminalde doldurulur.

Kurulan yapının içinde iki adet çalıştırılacak kompanent var. Önce terminalden kök dizine girilip npm start yapılır. Sonra terminalden example klasörüne girilip npm start yapılır.

node.js güncel versiyonda example içinde verilen komut kata veriyor. Bunu önlemek için example/package.json script alanında start keyinin valuesinin sonundaki start kelimesi --openssl-legacy-provider start ile değiştirilir.

Oluşturduğumuz kütüphanenin denemesi example içinde import edilmiş olarak verilir. Biz de bunların üzerinde çalışacağız.

Publish İşlemleri

npmjs.org üzerinden paylaşmak için bir önceki konuda oluşturduğumuz yapı üzerinden bir kompanent oluşturacağız.

src/index.js içinde:

import React from 'react' 
import styles from './styles.module.css'

export const ExampleComponent = ({ text }) => {
  return <div className={styles.test}>Example Component: {text}</div>
} // bu hazır gelen modül

export const Button = (props) => {
  return(
    <button {...props}>{props.text}</button> {/* bu şekilde yazıldığında gönderilen tüm propları alır.  */}
  ) 
}
              

example/src/App.js içinde:

import React from 'react'

import { ExampleComponent, Button } from 'ravenui-test'
import 'ravenui-test/dist/index.css'

const App = () => {
  return (
  <>
    <ExampleComponent text="Create React Library Example 😄" /> // hazır gelen modül.
    <Button text="Click"/> // html button etiketinin alacağı özellikleri burada veremeyiz çünkü burada sadece tanımladığımız modülü kullanıyoruz. Bu özellikleri modül tanımı kısmında girerek kullanabiliriz veya prop olarak gönderip kullanabiliriz.
  </>
  )
}

export default App

              

Yayınlamak için npmjs.org a üye oluyoruz.

Terminale

npm login
              
yazılıp gelen form doldurulur.

terminale:

npm publish
              
yazılarak oluşturduğumuz kütüphane upload edilir.

Aynı isimde kütüphane var hatası alınırsa package.json dosyasından isim değiştirilebilir.

Semantic Versiyonlama

Versiyon numarası package.json üzerinden verilir. nokta ile ayrılan 3 sayıdan oluşur. 3.0.2 gibi.

En sondaki sayı ufak hataları, patch işlemlerini vs yaptığımızda değiştirilir.

Ortadaki sayı minör değişikliklerde arttırılır.

En baştaki sayı major değişiklikleri gösterir. Sistemin tamamen değiştiğini gösterir.

Versiyon değişikliği için terminale:

npm version patch --f
              
yazılarak sondaki sayı 1 arttırılır.
npm version minor --f
              
yazılarak ortadaki sayı 1 arttırılır.
npm version major --f
              
yazılarak baştaki sayı 1 arttırılır.

soldaki sayı arttığında sağdakiler sıfırlanır.

terminale

npm publish 
              
ile güncelleme gönderilir.

Kütüphanemizi kullanan kullanıcının kendi versiyonunu güncellemesi için terminale:

npm upgrade ravenui
              
yazılır.

Kullanıcının sistemi güncellemeyi kabul etmezse

npm upgrade ravenui --force
              
yazılır.

Surge.sh

Demo gösterimleri için ideal. Yetenekleri sınırlı

Önce surge.sh bilgisayara global olarak kurulur. Bunun için terminale:

npm install --global surge
              

Sonra deploy etmek istediğimiz dizine gelip terminale:

surge
                
yazılır.

Gelen ekrana email ve şifre girilir ve form doldurulur.

react projesi deploy edilmeden önce build yapılır. Bunun için terminale:

npm run build
                
yazılır.
Daha sonra bu dizine girilip surge komutu çalıştırılır.

Her güncelleme sonrası build işlemi tekrar yapılır ve ardından deploy edilir. Deploy sırasında yeni bir domain önerebilir. Biz eski domaini elle girerek mevcut siteyi güncelleyebiliriz.

Yukarıdaki işlemi her seferinde tekrar yapmaktansa package.json dosyasına kısa yol girebiliriz. Bunun için package.json>scripts alanına:

"deploy": "npm run build && surge"
                
eklenir. ve gerektiğinde
npm run deploy
ile çalıştırılır.

Netlify

Daha kapsamlı. Küçük ve orat ölçekli projeler için ideal.

Kayıt ol. giriş yap.

github üzerindeki repodan projeyi çekiyor.

React projesini yükleyebilmek için build işlemini netlify kendisi yapıyor.

sitede add new site yi tıklayarak ve oradaki yönergeleri takip ederek projemizi deploy edebiliriz.

deploy sırasında aldığım bir hata ve çözümü için tıklayınız

Bağlı olduğu git reposu güncellendiğinde netlify deploy işlemini tekrar yapar.

netlfy üzerinde kullanılan react projelerinde anasayfa dışında sayfa yenilenirse sayfa hata verir. Bu hata sayfa linklerinin backend üzerinde değil client üzerinde var olmasıdır. Bunu düzeltmek için projenin public dizininde _redirects dosyası oluşturulur ve içine

/*    /index.html   200
              
yazılır. Bu sayede tüm yönlendirmeler netlify tarafından algılanır.

Netlify'da Neler Yapabiliriz?

Custom domain

site setting>domain management>add custom domain

Deploydan önce test kodunu çalıştırmak

site settings>Build & deploy> Continuous Deployment kısmındaki build command satırını "npm test && npm run build" olarak güncellersek önce testi çalıştırır. Testi geçerse deploy yapar

Deploy Notifications

site settings>Build & deploy>Deploy notifications

Ortam Değişkenini Netlify'de Kullanmak

Önce ortam değişkeni oluşturuyoruz. bunun için kök dizinde .env dosyası oluşturduk. İçine:

REACT_APP_API_ENDPOINT=https://api.openweathermap.org
              
"REACT_APP" muhakkak yazmalı.

Bu şekilde oluşturulan ortam değişkeni proje içinde istenildiği yerde:

process.env.REACT_APP_API_ENDPOINT
              
olarak kullanılır.

deploy edildiğinde ortam değişkenlerini github a gönderilmez (.gitignore). Bu nedenle Netlify okuyamaz. Bu nedenle bu değişkenleri biz ekleriz.

site settings>Build & deploy>Enviroment kısmında ortam değişkenleri eklenir ve proje tetrar deloy edilir.

Önceki Deployları Görmek

Deploys ekranında tıklayıp açtığımız deployda permalink veya preview butonu ile daha önceki deploy versiyonlarını görebiliriz.

AWS EC2 Üzerine Deploy İşlemleri

AWS ye üye ol ve giriş yap. Arama ekranına ec2 yaz. Launch instance butonuna bas. Gelen listeden ubuntu server seç. free tier olanı seç.

Network setting içinde Create security group kısmında ssh protokolüne bir de http ve https ekle. Her üçü için de "Source type: anywhere" seçilir.

Launch butonuna basılır. Gelen ekranda key-pair oluşturulup bilgisayara indirilir. Bu dosya bizim daha sonra oluşturduğumuz sanal makinaya ulaşmamızı sağlayacak.

Instances içinden yeni sanal makinamzı bulup public IP address kopyalanır. Sonra key pairsin olduğu klasörde terminale:

ssh -i react-app.pem ubuntu@13.53.146.5
              
yazılır ve gelen soruya yes yazılır.

Bu işlem sırasında Warning: Permanently added "3.127. 945.215" (ECDSA) to the list of known hosts.
ubuntu@3.127.145.215: Permission denied (publickey).

hatası alırsak terminale:

chmod 400 react-app.pem
              
yazılarak dosyaya izin verilmiş olunur.

terminalde

ubuntu@ip-172-31-4-204:
yazıyorsa oluşturduğumuz sanal makinaya bağlandığımızı gösterir.

terminale

sudo apt-get update
              
ile ubuntu paketleri güncellenir.

node.js kurmak için terminale:

curl -fsSL https://deb.nodesource.com/setup_14.x | sudo -E bash - &&\
sudo apt-get install -y nodejs
              
komutunu yazdım.
Bu komutları node.js dökümantasyonundan bulduk. İhtiyaca uygun olanı oradan sağlayabiliriz.

terminale:

sudo apg-get install ngnix
              
yazarız. Bu aşamada public IP tarayıcıya yazılırsa bizi Welcome to nginx! sayfası karşılar.

wiki: "Nginx; yüksek eş zamanlı çalışma kabiliyeti, yüksek performans ve düşük hafıza kullanımına odaklanılarak tasarlanmış bir Web sunucusudur. Aynı zamanda ters vekil sunucusu, yük dengeleyici ve HTTP ön belleği olarak da kullanılabilir."

projeyi yüklemek için terminale:

git clone <proje adresi>
              

Klonlanma tamamlandıktan sonra terminale:

dir // yazarak mevcut dosya görüntülenir.
cd <dosya_adı> // yazılarak içine girilir.
npm i // ile gerekli paketler kurulur.
              

npm run build
              
ile build oluşturulur.

terminale:

sudo vi /etc/nginx/sites-available/default
yazarak nginx ayar sayfası açıldı.
sayfanın root kısmına build'imizin adresini yazıyoruz.
root /home/ubuntu/weather2/build;
              

service nginx reload
              
yazılarak nginx terkrar başlatılır.

Ortam değişkenlerini de servere yüklemek için terminale:

vi .env
              
ile env dosyası oluşturulur. içine
REACT_APP_API_ENDPOINT=https://api.openweathermap.org
              
yazıp kaydedip kapatıyoruz (esc -> :wq)

tekrar build oluşturuyoruz ve nginx terkrar başlatılır

daha önce deploy ettiğimizde sayfa değiştirip yenile yaptığımızda hata alıyorduk. Bu hatayı almamak için:

sudo vi /etc/nginx/sites-available/default
              
içindeki
try_files $uri $uri/ =404;
              
kodunu
try_files $uri $uri/ =index.html;
              
olarak güncellenir ve nginx tekrar başlatılır

git reposu güncellendiğinde güncel hali almak için terminale:

sudo git pull
              
sonra
npm run build
              
ile build oluşturulur.

Uygulama Tanıtımı

Ürünlerin listelendiği ve sipariş edildiği bir e-ticaret sitesi taslağı yapacağız.

Giriş yapılacak, sepete ürün eklenecek ve satın alınacak.

Ürünün detay sayfası var.

Listede aşağı inildikçe daha fazla seçenek açılacak.

Sipariş verince admin tarafına düşecek ve orada yönetilecek.

Kullanılacak teknoloji: React Router, React Query, Context, JWT (auth), Chakra (UI), Ant Design (UI), Formik, mongoDB, redis

Backend'e Genel Bakış

Backend hazır verildi. Terminalde backend dizinine girilip

npm i
ile gerekli modülleri kuruldu.
npm i -D nodemon
ile nodemon kuruldu
npm dev
ile başlatıldı.

Backendin çalışması için mondoDB ve redis yüklü olmalı.

redisin çalışması için terminale:

server-redis
yazıyoruz.

src içine .env oluşturup içine:

MONGO_URI=mongodb://localhost:27017
tanımı girilir.

mongoDB içinde test adında bir database oluşturulup orders, users ve products koleksiyonları oluşturulur ve hazır veriler buralara eklenir.

Postman kullanılarak backend test edilebilir. Gönderilen işleme nasıl bir geri dönüş sağladığını görebiliyoruz.

Login işlemi sırasında bir accessToken ve refreshToken oluşturuluyor. Bu veriler local storage üzerinde tutulabilir ve kullanıcı bir güncelleme yapmak istediğinde auth işlemi için kullanılabilir. accessToken'ın ömrü kısa. Bu ömür bittiğinde refreshToken üzerinden yenilenir. refreshToken da yoksa tekrar login ister. Bu kısmın düzgün çalışması için ortam değişkeni olarak .env dosyasına

JWT_SECRET=sdgkMKEVlm3v23kl_n423vGG3b_YVnm234xnv23
JWT_REFRESH_SECRET=rerv1jv15v1CVBnasd23jnv1j3123nvrqwr23
              
eklenir. keyler aynı kalmak koşuluyla value istediğimiz gibi girilebilir.

Chakra UI Kurulumu

npx create-react-app client
              

npm install react-router-dom
              
ile react router v6 kuruldu ve App.js içinde yerleştirildi.

App.js içinde

import "./App.css" 
import { BrowserRouter as Router, Link, Route, Routes } from "react-router-dom"; 
import Navbar from "./components/Navbar";

function App() {
  return (
    <Router>
      <h1>Welcome</h1>
      <Navbar/>
      <div id="content">
      <Routes>
        <Route path="/" element={<Home />} />
      </Routes>
      </div>
    </Router>
  );
}

function Home() {
  return <h2>Home</h2>
}

export default App;

              

App.css içine aşağıdaki tanım girildi:

#content{
  padding: 15px;
}
              

src/components/Navbar içine index.js ve styles.module.css dosyası oluşturuldu.
index.js içine:

import styles from "./styles.module.css";
import { Link } from "react-router-dom";

function Navbar() {
  return (
    <div>
      <nav className={styles.nav}>
        <div className={styles.left}>
          <div className="logo">
            <Link to="/">eCommance</Link>
          </div>
          <ul className={styles.menu}>
            <li>
              <Link>Products</Link>
            </li>
          </ul>
        </div>

        <div className="right">right</div>
      </nav>
    </div>
  );
}

export default Navbar;

              

styles.module.css içine:
             
.nav{
  padding: 13px;
  display: flex;
  justify-content: space-between;
  border-bottom: solid 1px #e2e8f0;
  line-height: 2px;
  align-items: center; 
}

.nav .left{
  display: flex;
}

.nav .left .menu{
  display: flex;
  margin-left: 40px;
}

.nav .left .menu li a {
  color: #4a5568;
  text-decoration: none;
  font-size: 1.1rem;
  padding: 3px 16px;
  display: block;
  font-size: 16px;
}

.nav .left .menu li a:hover {
  color: black;
}
              
sitil tanımları girilir.

Default stil tanımlarından kurtulmak için reset css içindeki tanımlar kök dizinde reset.css dosyasına eklenir. Bu dosya kök dizindeki index.js dosyasına en alttaki stil tanımı olacak şekilde eklenir.

Chakra UI

Terminale

npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion
              

Tüm kompanentleri

<ChakraProvider></ChakraProvider>
ile sarmalıyoruz. Bunu index.js veya App.js üzerinden yapabiliriz.

Chakra UI için kullandığımzı yapılar için dökümantasyon sayfasını kullanıyoruz.

components/Navbar içine buton eklemek için önce butonu import ediyoruz.

import { Button, ButtonGroup } from "@chakra-ui/react"; // Sonra bu butonu Link etiketi ile sarmalayıp kullanıyoruz.
<div className={styles.right}>
  <Link to="/singup">
    <Button colorScheme="pink">Register</Button>
  </Link>
  <Link to="/singin">
    <Button colorScheme="pink">Login</Button>
  </Link>
</div>
              

Butonların bitişik olmaması için Navbar/styles.module.css dosyasına:

.nav .right a:first-child {
  margin-right: 5px;
}
              
eklenir.

Singin ve Singup kompanentlerinin şimdilik yerini tutmaları için components/pages/Auth klasörü içinde Singin ve Singup klasöründe index.js oluşturulup örnek kompanentler yapıldı.

Bu kompanentlere yönlendirme yapılması için App.js içinde yönlendirilecek kompanentler import edildi:

import Singin from "./pages/Auth/Singin";
import Singup from "./pages/Auth/Singup";
              

Routes etiketi içinde yönlendirmeler yapıldı.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/singin" element={<Singin/>} />
  <Route path="/singup" element={<Singup />} />
</Routes>
              

Products Ekranının Hazırlanması

App.js üzerinde Routes altında

<Route path="/" element={<Products />} />
yönlendirmesi yapıldı.

Components/Navbar içinde

<Link to="/">Products</Link>
yönlendirmesi yapıldı.

src/pages/Products klasöründe index.js oluşturuldu. İçinde kullanmak için components/Card klasörü içinde index.js oluşturuldu:

import { Box, Image, Button } from "@chakra-ui/react";
import { Link } from "react-router-dom";

function Card() {
  return (
    <Box borderWidth="1px" borderRadius="lg" overflow="hidden" p="3"> // Box chakra-ui kompanentidir. Tüm yapı Box ile sarıldı.
      <Link to="#/"> // Yapı link haline getirildi.
        <Image src="https://picsum.photos/400/200" alt="product" />

        <Box p="6">
          <Box d="plex" alignItems="baseline">
            27/03/2023
          </Box>

          <Box mt="1" fontWeight="semibold" as="h4" lineHeight="tight">
            MacBook Pro
          </Box>
          <Box>100 TL</Box>
        </Box>
      </Link>

      <Button colorScheme="pink">
        Add to basket
      </Button>
    </Box>
  );
}

export default Card;

              

Card taslağı Products/index.js içinde (şimdilik) yer tutucu olarak kullanıldı.
import { Grid } from "@chakra-ui/react"; 
import Card from "../../components/Card"; 

function Products() {
  return (
    <div>
      <Grid templateColumns="repeat(3, 1fr)" gap={4}> // Grid yapısı chakra-uı kompanentidir. gap boşluk miktarını repaet kaç kolon oduğunu verir. 
        <Card />
        <Card />
        <Card />
        <Card />
        <Card />
      </Grid>
    </div>
  );
}

export default Products;

              
ve Product App.js içine import edildi.

React Query

State yönetim aracı. Dökümantasyon için tıklayınız

Terminale:

npm i @tanstack/react-query
              

src/index.js içinde import edilir ve

import React from "react"; 
import ReactDOM from "react-dom/client";
import "./index.css";
import "./reset.css";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; // ile import edildi

import { ChakraProvider } from "@chakra-ui/react";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

const queryClient = new QueryClient(); // ile yeni sorgu istemcisi oluşturulur.  

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}> // ile tüm kompanentleri QueryClientProvider ile sarıyoruz. istemci olarak da yukaarıda oluşturduğumuz sorgu istemcisini gösteriyoruz. 
      <ChakraProvider>
        <App />
      </ChakraProvider>
    </QueryClientProvider>
  </React.StrictMode>
);
              

src/pages/Products/index.js içinde veriyi çekip map fonksiyonu ile kullanıyoruz.

import { Grid } from "@chakra-ui/react";
import { useQuery } from "@tanstack/react-query"; // ile react query içinden ihtiyaç duyduğumuz kısmı import ettik. 

import { fetchProductList } from "../../api"; // ile fetch işlemini başka bir dosyada yapıp import ettik. 
import Card from "../../components/Card";

function Products() {
  const { isLoading, error, data } = useQuery({
    queryKey: ['products'],  // daha sonra lazım olacak. 
    queryFn: fetchProductList, // fetch işlemi yapılan fonksiyon. Bu kısımda fonksiyonun kendisi de yazılabilirdi. 
  })

  if (isLoading) return 'Loading...' // yükleme devam ediyorsa çalışır. 

  if (error) return 'An error has occurred: ' + error.message // hata varsa çalışır. 
  return (
    <div>
      <Grid templateColumns="repeat(3, 1fr)" gap={4}>
        {
          data.map((item, key) => <Card key={key} item={item}/>) // Card kompanentine prop olarak item gönderildi. 
        }
      </Grid>
    </div>
  );
}

export default Products;

              

src/api.js içinde fetch işlemi yapıldı. Bunun için axios kullanıldı.
terminale:

npm i axios
              

api.js içine:
import axios from "axios";
export const fetchProductList = async() =>{
    const {data} = await axios.get("http://localhost:4000/product")

    return data
}
              

Prop olarak Products dosyasından gelen veri Card kompanentinde ilgili yerlere yerleştirildi. Tarih bilgisinin formatını ayarlamak için moment paketi kullanıldı.
terminale:

npm i moment
              

Card/index.js dosyasına:
import { Box, Image, Button } from "@chakra-ui/react";
import moment from "moment";
import { Link } from "react-router-dom";

function Card({item}) {
  return (
    <Box borderWidth="1px" borderRadius="lg" overflow="hidden" p="3">
      <Link to="#/">
        <Image src={item.photos[0]} alt="product" />

        <Box p="6">
          <Box d="plex" alignItems="baseline">
            {moment(item.createdAt).format("DD/MM/YYYY")} // moment ile tarih istenilen formatta yazıldı.
          </Box>

          <Box mt="1" fontWeight="semibold" as="h4" lineHeight="tight">
            {item.title}
          </Box>
          <Box>{item.price} TL</Box>
        </Box>
      </Link>

      <Button colorScheme="pink">
        Add to basket
      </Button>
    </Box>
  );
}

export default Card;

              

React Query Dev Tools

React Query nin geliştirme aşamasında bize yardımcı olması için verdiği bir geliştirme aracı.

Eski sürümlerde React Query içindeymiş. Şimdi haricen yükleniyor.
terminale

npm i @tanstack/react-query-devtools
              

QueryClientProvider yapısını kurduğumuz kısımda (örneğimizde kök dizindeki index.js) QueryClientProvider yapısının kapanış parantezinin hemen üstüne eklenerek çaıştırılır.

... 
  <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
              

Bunun sonucunda web sayfası sol altta bir logo belirir ve tıklandığında geliştirme aracını açar.

pages/Products/index.js içinde Product fonksiyonunda kullandığımız

queryKey: ['products']
ifadesi bu geliştirme aracı için gereklidir.

Ürün Detay Sayfasının Geliştirilmesi

App.js içinde yönlendirme yapılır:

<Route path="/product/:product_id" element={<ProductDetail />} />
              

Yönlendirmenin yapıldığı ProductDetail kompanenti import edilir.

Product sayfasında kullanılan Card kompanentindeki link düzenlenir:

<Link to={`product/${item._id}`}>
              

ProductDetail kompanenti için resim galerisi paketi kuruldu.

Terminale:

npm i react-image-gallery
              

ProductDetail/index.js içine:

import { useQuery } from "@tanstack/react-query";
import { useParams } from "react-router-dom";
import { fetchProduct } from "../../api";
import { Box, Text, Button } from "@chakra-ui/react";
import moment from "moment";
import ImageGallery from 'react-image-gallery';

function ProductDetail() {
  const { product_id } = useParams(); // alınan veri router üzerinde gönderilen ad (product_id) ile alınır.

  const { isLoading, isError, data } = useQuery(["product", product_id], () => 
    fetchProduct(product_id) 
  ); // Products sayfasındaki işlemin farklı yazılmışı.

  if (isLoading) { 
    return <div>Loading...</div>; 
  }

  if (isError) {
    return <div>Error!</div>;
  }

  const images = data.photos.map((url) => ({ original: url})) // array olarak verilen veri array içinde object olarak düzenlendi 
  
  return <div>
    <Button colorScheme="pink">
      Add to basket
    </Button>
    <Text as="h2" fontSize="2xl">
      {data.title}
    </Text>
    <Text>
      {moment(data.createdAt).format("DD/MM/YYYY")}
    </Text>
    <p>
      {data.description}
    </p>

    <Box margin="10">
      <ImageGallery items={images} />
    </Box>
  </div>;
}

export default ProductDetail;

              

ImageGallery kompanentinin düzgün çalışması için herhangi index.css içine

@import "~react-image-gallery/styles/css/image-gallery.css";
girilir.

api.js içindeki fertch işlemleri için client kök dizinindeki .env dosyasına:

REACT_APP_BASE_ENDPOINT=http://localhost:4000 
lokal değişkeni tanımlandı.

api.js içinde ProductDetail içine kullanılacak fetch işlemi eklendi.

export const fetchProduct = async(product_id) =>{ 
  const {data} = await axios.get(`${process.env.REACT_APP_BASE_ENDPOINT}/product/${product_id}`)

  return data
}
              

useInfiniteQuery

Bu kısımda sayfanın en altına buton koyup daha fazla ürün getirme işlemini yapacağız.

api.js üzerindeki fetchProductList fonksiyonu düzenlendi:

export const fetchProductList = async ({ pageParam = 1 }) => { 
  const { data } = await axios.get( 
    `${process.env.REACT_APP_BASE_ENDPOINT}/product?page=${pageParam}` 
  );

  return data;
};
              

pages/Products/index.js aşağıdaki gibi tekrar düzenlendi.

import { Box, Grid, Flex, Button } from "@chakra-ui/react";
import { useInfiniteQuery } from "@tanstack/react-query";
import React from "react";

import { fetchProductList } from "../../api";
import Card from "../../components/Card";

function Products() {
  const { // Bu kısımdaki tanımlar useInfiniteQuery fonkisyonunda tanımlı. 
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery(["products"], fetchProductList,
    {
    getNextPageParam: (lastGroup, allGroup) => {
      const morePagesExist = lastGroup?.length === 12; // son getirilen grup var mı? eleman sayısı 12 mi? Buradaki 12 sayısı backend tarafında bir sayfada görünecek maksimum ürün sayısına atıftır. 
      if (!morePagesExist) {
        return;
      }

      return allGroup.length + 1; // allGroup.length 1 olarak başlar. morePagesExist true döndüğü her seferinde 1 arttırılır. 
    },
  }
  );

  if (status === "loading") return "Loading...";

  if (status === "error") return "An error has occurred: " + error.message;

  return (
    <div>
      <Grid templateColumns="repeat(3, 1fr)" gap={4}>
        {
          data.pages.map((group, i) => (
            <React.Fragment key={i}>
              {
                group.map((item) => (
                  <Box w="100" key={item._id}>
                    <Card item={item} />
                  </Box>
                ))
              }
            </React.Fragment>
          ))
        }

      </Grid>

      <Flex mt="10" justifyContent="center">
        <Button
          onClick={() => fetchNextPage()}
          isLoading={isFetchingNextPage}
          disabled={!hasNextPage || isFetchingNextPage}
        >
          {isFetchingNextPage
            ? "Loading more..."
            : hasNextPage
            ? "Load More"
            : "Nothing more to load"}
        </Button>
        <div>{isFetching && !isFetchingNextPage ? "Fetching..." : null}</div>
      </Flex>
    </div>
  );
}

export default Products;

              

Kurulan yapı useInfiniteQuery fonkisyonuna özgüdür. Kalıp olarak kullanıldığından fazla bir açıklama yer almamaktadır.

Kullanıcı Kayıt İşlemleri

Formik ve yup'u kuracağız.

npm i formik yup
              

api.js dosyası içinde register işlemi için gereken fetch işlemi tanımlanır.

export const fetchRegister = async (input) => { 
  const { data } = await axios.post(
    `${process.env.REACT_APP_BASE_ENDPOINT}/auth/register`,
    input
  );

  return data;
};
              

src/pages/Auth/Singup/index.js dosyası aşağıdaki şekilde düzenlenir.

import React from "react";
import {
  Flex,
  Box,
  Heading,
  FormControl,
  FormLabel,
  Input,
  Button,
  Alert,
} from "@chakra-ui/react";
import { useFormik } from "formik";
import validations from "./validation"; // validation.js içindeki validations tanımı import edilir ve kullanılır. Bu tanımlar aşağıda verildi. 
import { fetchRegister } from "../../../api";  // ile yukarıda tanımlanan fetch işlemi import edildi. 

function Singup() {
  const formik = useFormik({
    initialValues: {
      email: "",
      password: "",
      passwordConfirm: "",
    },
    onSubmit: async (values, bag) => { // values: formdaki datalar, bag: formda yapılabilecek bir takım işlemler (formu resetlemek gibi). biz yeni hata mesajı oluşturmak için kullandık. 
      try {
        const registerResponse = await fetchRegister({
          email: values.email,
          password: values.password,
        }); // passwordComfirm backend tarafında yok. Bunu göndermemek için values'in tamamı değil backend tarafından beklenen kısmı gönderilir.
      } catch (e) {
        bag.setErrors({ general: e.response.data.message === "This e-mail already using." ? "Bu mail zaten kullanılıyor": e.response.data.message });
      }
    },
    validationSchema: validations, // import edilen validasyon tanımları kullanıldı. 
  });
  return (
    <div>
      <Flex align="center" justifyContent="center" width="full">
        <Box pt={10}>
          <Box textAlign="center">
            <Heading>Sing Up</Heading>
          </Box>
          <Box my={5}>
            {formik.errors.general && (
              <Alert status="error">{formik.errors.general}</Alert>
            )}
          </Box>
          <Box my={5} textAlign="left">
            <form onSubmit={formik.handleSubmit}>
              <FormControl>
                <FormLabel>E-mail</FormLabel>
                <Input
                  name="email"
                  onChange={formik.handleChange}
                  onBlur={formik.handleBlur}
                  value={formik.values.email}
                  isInvalid={formik.touched.email && formik.errors.email} // true olduğunda yapı geçersiz demektir ve input farklı renk alır.
                />
                {formik.errors.email && formik.touched.email && (
                  <div>
                    <br />
                    <Alert status="error">{formik.errors.email}</Alert>
                  </div>
                )}
              </FormControl>
              <FormControl mt={4}>
                <FormLabel>Password</FormLabel>
                <Input
                  name="password"
                  type="password"
                  onChange={formik.handleChange}
                  onBlur={formik.handleBlur}
                  value={formik.values.password}
                  isInvalid={formik.errors.password && formik.touched.password}
                />
                {formik.errors.password && formik.touched.password && (
                  <div>
                    <br />
                    <Alert status="error">{formik.errors.password}</Alert>
                  </div>
                )}
              </FormControl>
              <FormControl mt={4}>
                <FormLabel>Password Confirm</FormLabel>
                <Input
                  name="passwordConfirm"
                  type="password"
                  onChange={formik.handleChange}
                  onBlur={formik.handleBlur}
                  value={formik.values.passwordConfirm}
                  isInvalid={
                    formik.errors.passwordConfirm &&
                    formik.touched.passwordConfirm
                  }
                />
                {formik.errors.passwordConfirm &&
                  formik.touched.passwordConfirm && (
                    <div>
                      <br />
                      <Alert status="error">{formik.errors.passwordConfirm}</Alert>
                    </div>
                  )}
              </FormControl>

              <Button mt={4} width="full" type="submit">
                Sing Up
              </Button>
            </form>
          </Box>
        </Box>
      </Flex>
    </div>
  );
}

export default Singup;

              

Validasyon tanımı için Singup/validation.js içine:

import * as yup from "yup";

const validations = yup.object().shape({
  email: yup.string().email("Geçerli bir email girin").required("Zorunlu alan"),
  password: yup
    .string()
    .min(5, "Parolanız en az 5 karakter olmalıdır")
    .required("Zorunlu alan"),
  passwordConfirm: yup
    .string()
    .oneOf([yup.ref("password")], "Parolalar uyuşmuyor")
    .required("Zorunlu alan"),
});

export default validations;

              

Giriş Yapıldığı Verisinin Sayfada Kullanılması

Giriş sırasında bilginin tutulması için src/context/AuthContext.js dosyası oluşturuldu:

import { useState, createContext, useEffect, useContext } from "react"; 

const AuthContext = createContext();

const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loggedIn, setLoggedIn] = useState(false);

  const login = (data) => {
    setLoggedIn(true);
    setUser(data.user);
  };

  const values = {
    loggedIn,
    user,
    login,
  };
  return <AuthContext.Provider value={values}>{children}</AuthContext.Provider>;
};

const useAuth = () => useContext(AuthContext);

export { AuthProvider, useAuth };

              

Kök dizinde AuthProvider import edildi ve <App /> kompanentini sarmak için kullanıldı.

import { AuthProvider } from "./contexts/AuthContext";
...
...
  <AuthProvider>
    <App />
  </AuthProvider>
...
              

Singup/index.js içinde useAuth() fonksiyonu import edildi ve kullanıldı.

...
import { useAuth } from "../../../contexts/AuthContext";
...
function Singup() {
  const { login } = useAuth();
  ...
  try {
    const registerResponse = await fetchRegister({
      email: values.email,
      password: values.password,
    });
    login(registerResponse)
    ...
              

Navbar/index.js içinde useAuth() fonksiyonu import edildi ve kullanıldı.

import { useAuth } from "../../contexts/AuthContext";
function Navbar() {
  const { loggedIn } = useAuth();
  ...
  <div className={styles.right}>
    {!loggedIn && (
      <>
        <Link to="/singup">
          <Button colorScheme="pink">Register</Button>
        </Link>
        <Link to="/singin">
          <Button colorScheme="pink">Login</Button>
        </Link>
      </>
    )}
    {
      loggedIn && (
        <>
        <Link to="/profile">
          <Button>Profile</Button>
        </Link>
      </>
      )
    }

  </div>
  ...
              

Login Durumunun Sayfa Yenilendikten Sonra Korunması

Bu durum backend tarafında korunur ancak şu anki hali ile client tarafı bu veriyi alamaz. Alması için fetch işlemi yapağız. Bunun için de header ile access-token göndermemiz gerekiyor. Bu işlemi bazı sorgularımızda yapacağız. İşlemi otomatikleştirmek için axios paketinin bir fonksiyonu var. Onu api.js dosyasının baş kısmına giriyoruz.

// Add a request interceptor 
axios.interceptors.request.use(
  function (config) {
    // Do something before request is sent
    const { origin } = new URL(config.url);

    const allowedOrigin = [process.env.REACT_APP_BASE_ENDPOINT]; // hangi endpointlere istek yapılırken bu düzenlemenin geçerli olduğunu belirttik.
    const token = localStorage.getItem("access-token")

    if(allowedOrigin.includes(origin)){
      config.headers.authorization = token
    }

    return config;
  },
  function (error) {
    // Do something with request error
    return Promise.reject(error);
  }
);
              

Detaylı okuma için tıklayınız.

Backendden gelecek profil bilgisi için api.js içinde fetch işlemi yapılır.

export const fetchMe = async () => {
  const { data } = await axios.get(
    `${process.env.REACT_APP_BASE_ENDPOINT}/auth/me`
  );

  return data;
};
              

fetch ile gelen veri contexts/AuthContext.js içinde yakalanır ve kullanılır.

import { useState, createContext, useEffect, useContext } from "react";
import { fetchMe } from "../api"; //  fetchMe fonksiyonu import edildi. 
import { Flex, Spinner } from "@chakra-ui/react"; //  veri alınana kadar gelen loading kısmı için gereklidir.

const AuthContext = createContext();

const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loggedIn, setLoggedIn] = useState(false);
  const [loading, setLoading] = useState(true); //  başlangıçta true alır. fetchMe fonksiyonu ile veri alnınca false a döner. Veri alınırlen çıkan loading yapısını kullanmayı sağlar. 
  useEffect(() => {
    (async () => { //  işlemlerin asenkron olabilmesi için bir fonksiyonla tanımlanması gerekiyor. Bu fonksiyonun okunur okunmaz çalışması için anonim fonksiyon yapısı kullanıldı. 
      try {
        const me = await fetchMe(); // fetchMe fonksiyonundan veri gelene kadar bekler. 
        setLoggedIn(true);
        setUser(me);
        setLoading(false);
        console.log("me", me);
      } catch (e) {
        setLoading(false);
      }
    })();
  }, []);

  const login = (data) => {
    setLoggedIn(true);
    setUser(data.user);

    localStorage.setItem("access-token", data.accessToken);
    localStorage.setItem("refresh-token", data.refreshToken); //  login işlemi sırasında access-token ve refresh-token ibarelerini localStorage üzerine kaydeder. 
  };

  const values = {
    loggedIn,
    user,
    login,
  };

  if (loading) {  // Loading true iken görülecek olan spiner.
    return (
      <Flex justifyContent="center" alignItems="center" height="100vh">
        <Spinner
          thickness="4px"
          speed="0.65s"
          emptyColor="gray.200"
          size="xl"
          color="red.500"
        />
      </Flex>
    );
  }

  return <AuthContext.Provider value={values}>{children}</AuthContext.Provider>;
};

const useAuth = () => useContext(AuthContext);

export { AuthProvider, useAuth };

              

Giriş işlemi olduktan sonra ortaya çıkan Profile butonuna yer tutucu bir kompanent hazırlandı ve App.js üzerinden yönlendirmesi yapıldı.

Çıkış İşlemleri

api.js içinde logout için fetch işlemi tanımlanır.

export const fetchLogout = async () => {
  const { data } = await axios.post(
    `${process.env.REACT_APP_BASE_ENDPOINT}/auth/logout`,
    {
      refresh_token: localStorage.getItem("refresh-token"),
    }
  );

  return data;
};
              

AuthContext.js içinde logout fonksiyonu tanımlanır ve values ile gönderilir.

const logout = async(cb) => {
  setLoggedIn(false);
  setUser(null);

  await fetchLogout()
  
  localStorage.removeItem("access-token")
  localStorage.removeItem("refresh-token")

  cb() // parametre olarak alınan cb çalıştırılır. Tanımı logout fonksiyonunun kullanıldığı yerde yapıldı.
}

const values = {
  loggedIn,
  user,
  login,
  logout,
};
              

Profile/index.js içinde logout butonu konuldu.

import { useAuth } from "../../contexts/AuthContext";
import { useNavigate } from "react-router-dom";

import { Text, Button } from "@chakra-ui/react";

function Profile() {
  const { user, logout } = useAuth();

  let navigate = useNavigate();  //react-router-dom içinden alınan useNavigate() tanımı değişkene atandı. 
  const handleLogout = async () => {
    logout(() => {
      navigate("../"); // tanımlanan değişken sayfa yönlendirmek için kullanılır.  
    });
  };

  return (
    <div>
      <code>
        <Text fontSize={22}>Profile</Text>
        {JSON.stringify(user)}
        <br />
        <br />
        <Button colorScheme="pink" variant="solid" onClick={handleLogout}>
          Logout
        </Button>
      </code>
    </div>
  );
}

export default Profile;

              

Protected Routes

Derste react-router-dom v5 üzerinden anlatılıyor. v6 için tıklayın.

pages/ProtectedRoute.js dosyası oluşturuldu ve içine

import {Navigate, Outlet} from 'react-router-dom'

import { useAuth } from '../contexts/AuthContext'

function ProtectedRoute() {
    const {loggedIn} = useAuth()
  return (
    loggedIn ? <Outlet/> : <Navigate to ='/'/>
  )
}

export default ProtectedRoute
              

ProtectedRoute kompanenti app.js içine import edildi ve Routes>Route olarak kullanıldı. Korunmak istenen link de Outlet olarak yazıldı.

<Routes>
  <Route path="/" element={<Products />} />
  <Route path="/product/:product_id" element={<ProductDetail />} />
  <Route path="/singin" element={<Singin />} />
  <Route path="/singup" element={<Singup />} />
  
    <Route element={<ProtectedRoute/>}>
      <Route path="/profile" element={<Profile />} />
    </Route>
  
</Routes>
              

Login İşlemleri

api.js içinde login işlemi için gereken fetch işlemi tanımlanır.

export const fetchLogin = async (input) => {
  const { data } = await axios.post(
    `${process.env.REACT_APP_BASE_ENDPOINT}/auth/login`,
    input
  );

  return data;
};
              

Auth/Singin/index.js dosyası Singup/index.js dosyası referans alınarak oluşturuldu ve modifiye edildi.

import React from "react";
import {
  Flex,
  Box,
  Heading,
  FormControl,
  FormLabel,
  Input,
  Button,
  Alert,
} from "@chakra-ui/react";
import { useFormik } from "formik";
import validations from "./validation";
import { fetchLogin } from "../../../api";
import { useNavigate } from "react-router-dom";

import { useAuth } from "../../../contexts/AuthContext";

function Singin() {
  const { login } = useAuth();

  const navigate = useNavigate();

  const formik = useFormik({
    initialValues: {
      email: "",
      password: "",
    },
    onSubmit: async (values, bag) => {
      console.log(values);
      try {
        const loginResponse = await fetchLogin({
          email: values.email,
          password: values.password,
        });
        login(loginResponse);

        navigate("../profile");
        console.log("res", loginResponse);
      } catch (e) {
        bag.setErrors({
          general:
            e.response.data.message === "email or password not correct"
              ? "email veya parola hatalı"
              : e.response.data.message === "The email address was not found."
              ? "email bulunamadı"
              : e.response.data.message,
        });
      }
    },
    validationSchema: validations,
  });
  return (
    <div>
      <Flex align="center" justifyContent="center" width="full">
        <Box pt={10}>
          <Box textAlign="center">
            <Heading>Sing In</Heading>
          </Box>
          <Box my={5}>
            {formik.errors.general && (
              <Alert status="error">{formik.errors.general}</Alert>
            )}
          </Box>
          <Box my={5} textAlign="left">
            <form onSubmit={formik.handleSubmit}>
              <FormControl>
                <FormLabel>E-mail</FormLabel>
                <Input
                  name="email"
                  onChange={formik.handleChange}
                  onBlur={formik.handleBlur}
                  value={formik.values.email}
                  isInvalid={formik.touched.email && formik.errors.email}
                />
                {formik.errors.email && formik.touched.email && (
                  <div>
                    <br />
                    <Alert status="error">{formik.errors.email}</Alert>
                  </div>
                )}
              </FormControl>
              <FormControl mt={4}>
                <FormLabel>Password</FormLabel>
                <Input
                  name="password"
                  type="password"
                  onChange={formik.handleChange}
                  onBlur={formik.handleBlur}
                  value={formik.values.password}
                  isInvalid={formik.errors.password && formik.touched.password}
                />
                {formik.errors.password && formik.touched.password && (
                  <div>
                    <br />
                    <Alert status="error">{formik.errors.password}</Alert>
                  </div>
                )}
              </FormControl>

              <Button mt={4} width="full" type="submit">
                Sing In
              </Button>
            </form>
          </Box>
        </Box>
      </Flex>
    </div>
  );
}

export default Singin;

              

Sepete Atma İşlemleri

contexts/BasketContext.js dosyası oluşturuldu ve içine

import { useState, createContext, useContext, useEffect } from "react";

const BasketContext = createContext();

const BasketProvider = ({ children }) => {
  const [items, setItems] = useState([]);

  const addToBasket = (data, findBasketItem) => { //  data ile ürün verisi, findBasketItem ile de ürünün sepette olma durumu alınır.
    if (!findBasketItem) {
      return setItems((prev) => [...prev, data]);
    }
    const filtered = items.filter((item) => item._id !== findBasketItem._id);
    setItems(filtered);
  };

  const values = {
    items,
    setItems,
    addToBasket,
  };
  return (
    <BasketContext.Provider value={values}>{children}</BasketContext.Provider>
  );
};

const useBasket = () => useContext(BasketContext);

export { BasketProvider, useBasket };

              

Bu contexti kullanabilmek için kök dizindeki index.js içindeki App kompanenti BasketProvider ile sarmalanır.

Navbar/index.js içinde giriş yapıldıya se sepette ürün varsa Basket adında bir buton görünmesi ve bu butonda sepetteki ürün sayısının da yazılması sağlanır.

<div className={styles.right}>
  {!loggedIn && (
    <>
      <Link to="/singup">
        <Button colorScheme="pink">Register</Button>
      </Link>
      <Link to="/singin">
        <Button colorScheme="pink">Login</Button>
      </Link>
    </>
  )}
  {loggedIn && (
    <>
    
      {
        items.length > 0 && (
          <Link to="/basket">
            <Button colorScheme="pink" variant="outline">
              Basket ({items.length})
            </Button>
          </Link>
        )
      }
    
      <Link to="/profile">
        <Button>Profile</Button>
      </Link>
    </>
  )}
</div>
              

pages/ProductDetail/index.js dosyasındaki Add to basket butonu için gerekli düzenleme yapıldı.

... 
import { useBasket } from "../../contexts/BasketContext";
...
function ProductDetail() {
  const { product_id } = useParams();
  const { addToBasket, items } = useBasket();
  ...
  const findBasketItem = items.find((item) => item._id === product_id);

  return (
    <div>
      
        <Button colorScheme={findBasketItem ? "pink": "green"} onClick={() => addToBasket(data, findBasketItem)}>
        {findBasketItem ? "Remove to basket" : "Add to basket"}
      </Button>
      
      <Text as="h2" fontSize="2xl">
        ...
              

Product sayfasındaki ürünlerin altındaki Add to basket butonunu aktifleştirmek için ykarıdakine benzer bir işlem uygulanır:

import { useBasket } from "../../contexts/BasketContext"; 
...
function Card({ item }) {
  const { addToBasket, items } = useBasket();

  const findBasketItem = items.find(
    (basket_item) => basket_item._id === item._id
  );
  return (
    ...
    <Button
    colorScheme={findBasketItem ? "pink" : "green"}
    variant="solid"
    onClick={() => addToBasket(item, findBasketItem)}
  >
    {findBasketItem ? "Remove from basket" : "Add to basket"}
  </Button>
  ...
              

Sepete alınan ürünlerin görüntüleneceği ve işlenebileceği Basket sayfası pages/Basket/index.js içinde oluşturulur.

import React from "react";
import { useBasket } from "../../contexts/BasketContext";
import { Alert, Box, Button, Image, Text } from "@chakra-ui/react";
import { Link } from "react-router-dom";

function Basket() {
  const { items, removeToBasket } = useBasket(); // burada çağırılan removeToBasket fonksiyonu aşağıda anlatılacak.

  const total = items.reduce((acc, obj) => acc + obj.price, 0); // acc o anki toplam. obj items içindeki nesne. 0 da başlangıç değeri.
  return (
    <Box padding={5}>
      {items.length < 1 && (
        <Alert status="warning">You have not any items in your basket!</Alert>
      )}
      {items.length > 0 && (
        <>
          <ul style={{listStyleType: "decimal"}}>
            {items.map((item) => (
              <li key={item._id} style={{ marginBottom: 15 }}>
                <Link to={`/product/${item._id}`}>
                  <Text fontSize={18}>
                  {item.title} - {item.price} TL
                  </Text>
                  <Image
                    htmlWidth={200}
                    src={item.photos[0]}
                    loading="lazy"
                    alt="basket item"
                  />
                </Link>
                <Button
                  mt={2}
                  size="sm"
                  colorScheme="pink"
                  onClick={() => removeToBasket(item._id)}
                >
                  Remove from basket
                </Button>
              </li>
            ))}
          </ul>
          <Box mt={10}>
            <Text fontSize={22}>Total: {total} TL</Text>
          </Box>
        </>
      )}
    </Box>
  );
}

export default Basket;

              

contexts/BasketContext.js içinde removeToBasket fonksiyonu tanımlanır ve value olarak gönderilir.

const removeToBasket = (item_id) => {
  const filtered = items.filter((item) => item._id !== item_id);
  setItems(filtered);
};
const values = {
  items,
  setItems,
  addToBasket,
  removeToBasket,
};
              

Sayfa yönlendirmesi için app.js Routes içinde ProtectedRoute altına Basket elementi yerleştirilir.

<Routes>
  <Route path="/" element={<Products />} />
  <Route path="/product/:product_id" element={<ProductDetail />} />
  <Route path="/singin" element={<Singin />} />
  <Route path="/singup" element={<Singup />} />
  <Route element={<ProtectedRoute />}>
    <Route path="/profile" element={<Profile />} />
    <Route path="/basket" element={<Basket />} />
  </Route>
  <Route path="*" element={<Error404 />} /> {/* path tanımı yukarıdakilerin hiçbirine uymuyorsa bu kısım devreye girer ve Error404 sayfasına yönlendirir. */}
</Routes>
              

Error 404 sayfası için pages/Error404/index.js oluşturulur. İçine

import React from "react";
import {
  Alert,
  AlertIcon,
  AlertTitle,
  AlertDescription,
} from "@chakra-ui/react";

function Error404() {
  return (
    <div>
      <Alert status="error">
        <AlertIcon />
        <AlertTitle>Error 404</AlertTitle>
        <AlertDescription>
          This page was not found
        </AlertDescription>
      </Alert>
    </div>
  );
}

export default Error404;

              

Sipariş Oluşturma İşlemleri

pages/Basket/index.js içinde bir modal yerleştirildi ve siparişlerin database e gönderilmesinde kullanıldı.

import React, { useState } from "react"; 
import { useBasket } from "../../contexts/BasketContext"; import { Alert, Box, Button, Image, Text, Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton, useDisclosure, FormControl, FormLabel, Textarea, // modal ve içindeki bileşenler için gereken parçalar chakraUI üzerinden çekildi. } from "@chakra-ui/react"; import { Link } from "react-router-dom"; import { postOrder } from "../../api"; //api.js içinde gereken işlem tanımlandı ve import edildi. İşlem detayı aşağıda. function Basket() { const [address, setAddress] = useState(""); const { isOpen, onOpen, onClose } = useDisclosure(); const initialRef = React.useRef(null); // Bu iki tanım chakraUI modal tanımında kullanılıyor. const { items, removeToBasket, emptyBasket } = useBasket(); // emptybasket BasketContext içinde, sepeti işlem sonunda boşaltmak için oluşturuldu. Detayı aşağıda. const total = items.reduce((acc, obj) => acc + obj.price, 0); const handleSubmitForm = async () => { // Modal içindeki formu göndermek için tanımlanan fonksiyon. const itemIds = items.map((item) => item._id); const input = { //api.js üzerinden database e gönderilecek veri. address, items: JSON.stringify(itemIds), }; await postOrder(input); emptyBasket(); onClose(); // modalı işlemin sonunda kapatan fonksiyon. }; return ( <Box padding={5}> {items.length < 1 && ( <Alert status="warning">You have not any items in your basket!</Alert> )} {items.length > 0 && ( <> <ul style={{ listStyleType: "decimal" }}> {items.map((item) => ( <li key={item._id} style={{ marginBottom: 15 }}> <Link to={`/product/${item._id}`}> <Text fontSize={18}> {item.title} - {item.price} TL </Text> <Image htmlWidth={200} src={item.photos[0]} loading="lazy" alt="basket item" /> </Link> <Button mt={2} size="sm" colorScheme="pink" onClick={() => removeToBasket(item._id)} > Remove from basket </Button> </li> ))} </ul> <Box mt={10}> <Text fontSize={22}>Total: {total} TL</Text> </Box> <Button mt={2} size="sm" colorScheme="green" onClick={onOpen}> Order </Button> {/* modalı açan buton. */} <Modal initialFocusRef={initialRef} isOpen={isOpen} onClose={onClose}> //Modalın başlangıcı <ModalOverlay /> <ModalContent> <ModalHeader>Order</ModalHeader> <ModalCloseButton /> <ModalBody pb={6}> <FormControl> <FormLabel>Address</FormLabel> <Textarea ref={initialRef} placeholder="Address" value={address} onChange={(e) => setAddress(e.target.value)} /> </FormControl> </ModalBody> <ModalFooter> <Button colorScheme="blue" mr={3} onClick={handleSubmitForm}> // Formu gönderme işlemi yapan buton. Save </Button> <Button onClick={onClose}>Cancel</Button> </ModalFooter> </ModalContent> </Modal> </> )} </Box> ); } export default Basket;

api.js içinde gerekli fetch işlemi yapıldı ve import edildi.

export const postOrder = async (input) => {
  const { data } = await axios.post(
    `${process.env.REACT_APP_BASE_ENDPOINT}/order`,
    input
  );

  return data;
};
              

BasketContext/index.js içinde emptyBasket fonksiyonu tanımlandı ve value olarak gönderildi.

const emptyBasket = () => setItems([]);
              

Admin: Routing İşlemleri

Önce bir kullanıcın rolünü database üzerinden admin yapıyoruz. Hoca dersi react-router-dom v5 üzerinden anlatmış. Biz v6 ya göre düzenledik.

Navbar kompanentine kullanıcı admin ise görülecek bir buton yerleştirildi. Navbar/index.js

...
return (
  ...
  {loggedIn && (
    <>
    ...

    {
      user?.role === "admin" && (
        <Link to="/admin">
          <Button colorScheme="pink" variant="ghost">Admin</Button>
        </Link>
      )
    }
    ...
              

pages/Admin/index.js dosyası oluşturuldu ve içine:

import React from "react";
import { Link, Outlet } from "react-router-dom";
import styles from "./styles.module.css";
import { Box } from "@chakra-ui/react";

function Admin() {
  return (
    <div>
      <nav>
        <ul className={styles.adminMenu}>
          <li>
            <Link to="/admin">Home</Link>
          </li>
          <li>
            <Link to="/admin/orders">Orders</Link>
          </li>
          <li>
            <Link to="/admin/products">Products</Link>
          </li>
        </ul>
      </nav>

      <Box mt={10}>
        <Outlet />
      </Box>
    </div>
  );
}

export default Admin;

              

Admin/styles.module.css içine stil tanımları yapıldı.

.adminMenu{
  display: flex;
  padding: 0;
  margin: 0;
}

.adminMenu li{
  padding: 0 10px;
}
              

Admin olmayan kullanıcıların admin alanına ulaşamamalı için link korumalı olarak yazılmalı. Bunun için pages/ProtectedAdmin.js dosyası oluşturuldu. içine:

import { Navigate, Outlet } from "react-router-dom";

import { useAuth } from "../contexts/AuthContext";

function ProtectedAdmin() {
  const { loggedIn, user } = useAuth();
  return loggedIn && user.role === "admin" ? <Outlet /> : <Navigate to="/" />;
}

export default ProtectedAdmin;

              

Oluşturulan ProtectedAdmin kompanenti Add.js Routes alanında kullanıldı.

<Routes>
  ...
  <Route element={<ProtectedAdmin />}>
    <Route path="/admin" element={<Admin />}>
      <Route path="" element={<Home />} />
      <Route path="orders" element={<Orders />} />
      <Route path="products" element={<AdminProducts />} />
    </Route>
  </Route>
  ...
</Routes>
              

Routes alanında admin pathi içinde de nest yapısı kuruldu. Burası için linkler pages/Admin/index.js içinde verilmişti. Elementlerin Outlet yapısı da aynı sayfada.

admin altında yuvalanmış linkler için gereken kompanentler Admin klasöründe oluşturuldu. İçeriği daha sonra düzenlenecek.

Admin: Order Sayfası

api.js içinde gerekli fetch fonksiyonu tanımlanır.

export const fetchOrders = async () => {
  const { data } = await axios.get(
    `${process.env.REACT_APP_BASE_ENDPOINT}/order`
  );

  return data;
};
              

Alınan veri reactQuery yapısı ve chakraUI tablo görselleri ile pages/Admin/Orders.index.js dosyasında kullanılır. (Order detail kısmını modal olarak ben tasarladım :D )

import React, { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
  Table,
  Thead,
  Tbody,
  Tr,
  Th,
  Td,
  TableCaption,
  Text,
  Modal,
  ModalOverlay,
  ModalContent,
  ModalHeader,
  ModalFooter,
  ModalBody,
  ModalCloseButton,
  useDisclosure,
  Button,
} from "@chakra-ui/react";

import { fetchOrders } from "../../../api";
import { Link } from "react-router-dom";

function Orders() {
  const [order, setOrder] = useState([]); // modal içinde gönderilecek verinin statei 
  const total = order.reduce((acc, obj) => acc + obj.price, 0); // modal içinde kullanılacak verinin toplama işlemi 
  const { isOpen, onOpen, onClose } = useDisclosure(); // modal için gereken tanımlar. 
  const getDetail = (orderData) => {
    onOpen();
    setOrder(orderData);
  };

  const { isLoading, isError, data, error } = useQuery(
    ["admin:orders"],
    fetchOrders
  );

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <div>Error {error.message}</div>;
  }

  console.log("data", data);
  return (
    <div>
      <Text fontSize="2xl" padding={5}>
        Orders
      </Text>

      <Table variant="simple">
        <TableCaption>Order Page - Total order(s) = {data.length}</TableCaption>
        <Thead>
          <Tr>
            <Th>User</Th>
            <Th>Address</Th>
            <Th isNumeric>Items</Th>
            <Th></Th>
          </Tr>
        </Thead>
        <Tbody>
          {data.map((item) => (
            <Tr key={item._id}>
              <Td>{item.user.email}</Td>
              <Td>{item.adress}</Td>
              <Td isNumeric>{item.items.length}</Td>
              <Td>
                <Button onClick={() => getDetail(item.items)}>Detail</Button>
              </Td>
            </Tr>
          ))}
        </Tbody>
      </Table>

      <Modal isOpen={isOpen} onClose={onClose}> // Buradan itibaren benim tasarladığım modal başlıyor. 
        <ModalOverlay />
        <ModalContent>
          <ModalHeader>Modal Title</ModalHeader>
          <ModalCloseButton />
          <ModalBody>
            Order Detail: {order.length} <br />v
            <Table variant="simple">
              <TableCaption>Order Total Price = {total}</TableCaption>
              <Thead>
                <Tr>
                  <Th>Item</Th>
                  <Th isNumeric>Price</Th>
                  <Th></Th>
                </Tr>
              </Thead>
              <Tbody>
                {order &&
                  order.map((item) => (
                      <Tr key={item._id}>
                        <Td>{item.title}</Td>
                        <Td>{item.price} TL</Td>
                        <Td>
                          <Button>
                            <Link to={`../../product/${item._id}`}>
                              item detail
                            </Link>
                          </Button>
                        </Td>
                      </Tr>
                  ))}
              </Tbody>
            </Table>
          </ModalBody>

          <ModalFooter>
            <Button colorScheme="blue" mr={3} onClick={onClose}>
              Close
            </Button>
            <Button variant="ghost">Secondary Action</Button>
          </ModalFooter>
        </ModalContent>
      </Modal>
    </div>
  );
}

export default Orders;

              

Admin: Products Sayfası

Bu kısımda UI aracı olarak ant design kullanılacak. Terminale:

npm install antd
              

Yorumlarda material UI önerilmiş. İncelemekte fayda var.

Delete işlemi için gereken fetch işlemi api.js üzerinde tanımlandı.

export const deleteProduct = async (product_id) => {
  const { data } = await axios.delete(
    `${process.env.REACT_APP_BASE_ENDPOINT}/product/${product_id}`
  );

  return data;
};
              

Admin/AdminProducts/index.js içinde ant design üzerindeki tablo yapısı ile veriler ve edit - delete butonları tanımlandı. Delete için api.js de yazılan fonksiyon kullanıldı.

import { useMemo } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; // useMutation veri manupilasyonu için kullanılıyor.

import { fetchProductList, deleteProduct } from "../../../api";

import { Link } from "react-router-dom";
import { Text } from "@chakra-ui/react";
import { Table, Popconfirm } from "antd";

function Products() {
  const queryClient = useQueryClient();  // kök dizindeki index.js içindeki queryClient yapısına ulaşmayı sağlar.

  const { isLoading, isError, data, error } = useQuery(
    ["admin:products"],
    fetchProductList
  );

  const deleteMutation = useMutation(deleteProduct, {
    onSuccess: () => queryClient.invalidateQueries("admin:products"), // useQuery kullanırken tanımlanan "admin:products" bileşeninin queryClient içinde verilen tanımdan muaf olması sağlanır.
  });  // ilk parametrede işlem için gereken fetch fonksiyonu, ikinci parametrede options(işlemden önce, sonra yapılacak işlemler, tekrardan refetch yapılacak mı vs) girilir. 

  const columns = useMemo(() => {  // kolonların her seferinde refetch olmaması için useMemo() kullanıldı. 
    return [
      {
        title: "Title",
        dataIndex: "title",
        key: "title",
      },
      {
        title: "Price",
        dataIndex: "price",
        key: "price",
      },
      {
        title: "Created At",
        dataIndex: "createdAt",
        key: "createdAt",
      },
      {
        title: "Action",
        key: "action",
        render: (text, record) => ( // record o satıra denk gelen veriyi temsil eder
          <>
            <Link to={`/admin/products/${record._id}`}>Edit</Link>

            <Popconfirm  // ant design içindeki popconfirm kullanıldı. 
              title="Are you sure?"
              onConfirm={() => {  // onaylanması durumunda çalışacak fonksiyon. 
                deleteMutation.mutate(record._id, {
                  onSuccess: () => {  // onay işlemini başarılı olması durumunda çalışacak fonksiyon. 
                    console.log("success");
                  },
                });
              }}
              onCancel={() => console.log("iptal edildi")} // işlemi kullanıcının onaylamaması durumunda çalışacak fonksiyon. 
              okText="Yes"
              cancelText="No"
              placement="left" // popconfirm yerleşim şekli. 
            >
              <Link style={{marginLeft: 10}}>Delete</Link>
            </Popconfirm>
          </>
        ),
      },
    ];
  }, []);

  if (isLoading) {
    return 
Loading...
; } if (isError) { return
Error {error.message}
; } return ( <div> <Text fontSize="2xl" p="5"> Products </Text> <Table dataSource={data} columns={columns} rowKey="_id" />; // yukarıda tanımlanan tablonun yerleşimi. Return edilen datayı ve tanımlanan kolon yapısını kullanır. </div> ); } export default Products;

Admin: Product Update İşlemleri

App.js içinde admin/product sayfasındaki edit butonunda verilen bağlantı için routing yapıldı:

<Routes> 
  ...
  <Route element={<ProtectedAdmin />}>
    <Route path="/admin" element={<Admin />}>
      ...
      <Route path="products/:product_id" element={<AdminProductDetail />} />
    </Route>
  </Route>
  ...
</Routes>
              

api.js içinde adminProductDetail içindeki düzenlemeyi databasee gönderecek fetch işlemi tanımlandı

export const updateProduct = async (input, product_id) => {
  const { data } = await axios.put(
    `${process.env.REACT_APP_BASE_ENDPOINT}/product/${product_id}`,
    input
  );

  return data;
};
              

pages/Admin/AdminProductDetail/index.js içine

import React from "react";

import { useParams } from "react-router-dom";
import { fetchProduct, updateProduct } from "../../../api"; //  ürün detayını getiren ve yapılan güncellemeyi databasee iletecek fonksiyonlar import edildi. 
import { useQuery } from "@tanstack/react-query";
import {
  Box,
  Button,
  FormControl,
  FormLabel,
  Input,
  Text,
  Textarea,
} from "@chakra-ui/react"; //  Form bileşenlerinin görsel tasarımları için chakra-ui kullanıldı.
import { message } from "antd"; //  form submit edildiğinde çıkan spiner için ant-design kullanıldı. 
import { Formik, FieldArray } from "formik"; //  client tarafında arrayi düzenlemek için FieldArray import edildi. 
import validationSchema from "./validations"; // validasyon işlemleri: aşağıda taımlandı.

function AdminProductDetail() {
  const { product_id } = useParams();
  const { isLoading, isError, data, error } = useQuery(
    ["admin:product", product_id],
    () => fetchProduct(product_id)
  );
  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <div>Error {error.message}</div>;
  }

  const handleSubmit = async (values, bag) => { //  form tarafında submit için kullanılacak fonksiyon. 
    console.log("submitted");
    message.loading({ content: "Loading...", key: "product_update" });  //  ant-design tarafında çekilen spinner tanımı. 

    try {
      await updateProduct(values, product_id);
      message.success({ // işlemin başarılması durumunda spinner durumunda yapılacak güncelleme
        content: "The product successfully updated.",
        key: "product_update",
        duration: 2,
      });
    } catch (e) {
      message.error({ // işlemin başarısız olma durumunda spinner durumunda yapılan güncelleme
        content: "The product does not updated!",
        key: "product_update",
      });
    }
  };

  return (
    <div>
      <Text fontSize="2xl">Edit</Text>

      <Formik
        initialValues={{
          title: data.title,
          description: data.description,
          price: data.price,
          photos: data.photos,
        }}
        validationSchema={validationSchema}
        onSubmit={handleSubmit}
      >
        {({
          handleSubmit, // formik içinde kullanılan tanımlar.
          errors,
          touched,
          handleChange,
          handleBlur,
          values,
          isSubmitting,
        }) => (
          <>
            <Box>
              <Box my={5} textAlign="left">
                <form onSubmit={handleSubmit}>
                  <FormControl>
                    <FormLabel>Title</FormLabel>
                    <Input
                      name="title"
                      onChange={handleChange}
                      onBlur={handleBlur}
                      value={values.title}
                      disabled={isSubmitting}
                      isInvalid={touched.title && errors.title}
                    />
                    {touched.title && errors.title && (
                      <Text color="red">{errors.title}</Text>
                    )}
                  </FormControl>
                  <FormControl mt={4}>
                    <FormLabel>Description</FormLabel>
                    <Textarea
                      name="description"
                      onChange={handleChange}
                      onBlur={handleBlur}
                      value={values.description}
                      disabled={isSubmitting}
                      isInvalid={touched.description && errors.description}
                    />
                    {touched.description && errors.description && (
                      <Text color="red">{errors.description}</Text>
                    )}
                  </FormControl>
                  <FormControl mt={4}>
                    <FormLabel>Price</FormLabel>
                    <Input
                      name="price"
                      onChange={handleChange}
                      onBlur={handleBlur}
                      value={values.price}
                      disabled={isSubmitting}
                      isInvalid={touched.price && errors.price}
                    />
                    {touched.price && errors.price && (
                      <Text color="red">{errors.price}</Text>
                    )}
                  </FormControl>

                  <FormControl mt={4}>
                    <FormLabel>Photos</FormLabel>
                    <FieldArray
                      name="photos"  //  initualValues üzerinden gelen tanım
                      render={(arrayHelpers) => (
                        <div>
                          {values.photos &&
                            values.photos.map((photo, index) => (
                              <div key={index}>
                                <Input
                                  name={`photos.${index}`}
                                  value={photo}
                                  disabled={isSubmitting}
                                  onChange={handleChange}
                                  width="3xl"
                                />

                                <Button
                                  ml={4}
                                  type="button"
                                  colorScheme="red"
                                  onClick={() => arrayHelpers.remove(index)} // FieldArray>arrayHelpers yardımıyla remove işlemi yapıldı.
                                  isLoading={isSubmitting}
                                >
                                  Remove
                                </Button>
                              </div>
                            ))}

                          <Button
                            mt={5}
                            onClick={() => arrayHelpers.push("")} // FieldArray>arrayHelpers yardımıyla boş element ekleme işlemi yapıldı.
                            isLoading={isSubmitting}
                          >
                            Add a photo
                          </Button>
                        </div>
                      )}
                    />
                  </FormControl>
                  <Button
                    mt={4}
                    width="full"
                    type="submit" // formikin mevcut değişiklikle submit etmesi sağlanır.
                    isLoading={isSubmitting}
                  >
                    Update
                  </Button>
                </form>
              </Box>
            </Box>
          </>
        )}
      </Formik>
    </div>
  );
}

export default AdminProductDetail;

              

pages/Admin/AdminProductDetail/validations.js içine validasyon tanımları yapıldı.

import * as yup from "yup";

const editScheme = yup.object().shape({
    title: yup.string().required(),
    description: yup.string().min(5).required(),
    price: yup.number().required(),
})

export default editScheme;
              

Admin: Yeni Ürün Eklemek

App.js içinde gerekli routing yapıldı.

<Routes>
  ...
  <Route element={<ProtectedAdmin />}>
    <Route path="/admin" element={<Admin />}>
      ...
      <Route path="products" element={<AdminProducts />} />
      <Route path="products/new" element={<NewProduct />} />
      ...
    </Route>
  </Route>
  ...
</Routes>
              

api.js içinde gereken fetch fonksiyonu tanımlanır.

export const postProduct = async (input) => {
  const { data } = await axios.post(
    `${process.env.REACT_APP_BASE_ENDPOINT}/product/`,
    input
  );

  return data;
};
              

pages/Admin/AdminProducts/index.js içinde New Product butonu yerleştirilir.

...
return (
      <div>
        <Flex justifyContent="space-between" alignItems="center">
          <Text fontSize="2xl" p="5">
            Products
          </Text>

          
          <Link to={"/admin/products/new"}>
          <Button>New Product</Button>
        </Link>
          
        </Flex>
        <Table dataSource={data} columns={columns} rowKey="_id" />;
      </div>
    );
  }
  ...
              

pages/Admin/AdminProducts/new.js dosyası oluşturulur. İçindeki çoğu eleman pages/Admin/AdminProductDetail/index.js ve pages/Admin/AdminProducts/index.js dosyasından kopyalandı.

import React from "react";

import { postProduct } from "../../../api";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
  Box,
  Button,
  FormControl,
  FormLabel,
  Input,
  Text,
  Textarea,
} from "@chakra-ui/react";
import { message } from "antd";
import { Formik, FieldArray } from "formik";
import validationSchema from "./validations";

function NewProduct() {
  const queryClient = useQueryClient();

  const newProductMutation = useMutation(postProduct, { // ile fetch fonksiyonu yerleştirilir ve kullanılır.
    onSuccess: () => queryClient.invalidateQueries("admin:products"),
  });

  const handleSubmit = async (values, bag) => {
    console.log(values);
    message.loading({ content: "Loading...", key: "product_update" });

    const newValues = { // photos arrayi databasee stringfy edilerek gönderiliyor
      ...values, // tüm değerleri alır.
      photos: JSON.stringify(values.photos), // photos değerini stingfy şekliyle değiştirir.
    };

    newProductMutation.mutate(newValues, {
      onSuccess: () => {
        console.log("success");

        message.success({
          content: "The product successfully updated.",
          key: "product_update",
          duration: 2,
        });
      },
    });
  };
  
  return (
    <div>
      <Text fontSize="2xl">New Product</Text>

      <Formik
        initialValues={{
          title: "",
          description: "",
          price: "",
          photos: [],
        }}
        validationSchema={validationSchema}
        onSubmit={handleSubmit}
      >
        {({
          handleSubmit,
          errors,
          touched,
          handleChange,
          handleBlur,
          values,
          isSubmitting,
        }) => (
          <>
            <Box> // Form yapısı AdminProductDetail/index.js nin aynısı
              <Box my={5} textAlign="left">
                <form onSubmit={handleSubmit}>
                  <FormControl>
                    <FormLabel>Title</FormLabel>
                    <Input
                      name="title"
                      onChange={handleChange}
                      onBlur={handleBlur}
                      value={values.title}
                      disabled={isSubmitting}
                      isInvalid={touched.title && errors.title}
                    />
                    {touched.title && errors.title && (
                      <Text color="red">{errors.title}</Text>
                    )}
                  </FormControl>
                  <FormControl mt={4}>
                    <FormLabel>Description</FormLabel>
                    <Textarea
                      name="description"
                      onChange={handleChange}
                      onBlur={handleBlur}
                      value={values.description}
                      disabled={isSubmitting}
                      isInvalid={touched.description && errors.description}
                    />
                    {touched.description && errors.description && (
                      <Text color="red">{errors.description}</Text>
                    )}
                  </FormControl>
                  <FormControl mt={4}>
                    <FormLabel>Price</FormLabel>
                    <Input
                      name="price"
                      onChange={handleChange}
                      onBlur={handleBlur}
                      value={values.price}
                      disabled={isSubmitting}
                      isInvalid={touched.price && errors.price}
                    />
                    {touched.price && errors.price && (
                      <Text color="red">{errors.price}</Text>
                    )}
                  </FormControl>

                  <FormControl mt={4}>
                    <FormLabel>Photos</FormLabel>
                    <FieldArray
                      name="photos"
                      render={(arrayHelpers) => (
                        <div>
                          {values.photos &&
                            values.photos.map((photo, index) => (
                              <div key={index}>
                                <Input
                                  name={`photos.${index}`}
                                  value={photo}
                                  disabled={isSubmitting}
                                  onChange={handleChange}
                                  width="3xl"
                                />

                                <Button
                                  ml={4}
                                  type="button"
                                  colorScheme="red"
                                  onClick={() => arrayHelpers.remove(index)}
                                  isLoading={isSubmitting}
                                >
                                  Remove
                                </Button>
                              </div>
                            ))}

                          <Button
                            mt={5}
                            onClick={() => arrayHelpers.push("")}
                            isLoading={isSubmitting}
                          >
                            Add a photo
                          </Button>
                        </div>
                      )}
                    />
                  </FormControl>
                  <Button
                    mt={4}
                    width="full"
                    type="submit"
                    isLoading={isSubmitting}
                  >
                    Save
                  </Button>
                </form>
              </Box>
            </Box>
          </>
        )}
      </Formik>
    </div>
  );
}

export default NewProduct;

              

validasyon dosyası Admin/AdminProductDetail/validations.js içinden kopyalanarak düzenlendi.

import * as yup from "yup";

const NewProductScheme = yup.object().shape({
    title: yup.string().required(),
    description: yup.string().min(5).required(),
    price: yup.number().required(),
})

export default NewProductScheme;
              

Projeye Benim Katkım

Admin/Product sayfasında sadece 12 ürün görünüyor. Tüm ürünleri göstermek için backend tarafına müdahale ettim. routes/index sayfasında yeni bir yönlendirme yazıldı.

router.use('/product-all', productAll);
              

Bu yönlendirmeyi almak için routes/productAll.js dosyası oluşturuldu.

import express from "express";
import ProductAll from "../controllers/productAll";

const router = express.Router();

router.get("/", ProductAll.GetList);

export default router;
                

Bu yönlendirmeye response oluşturması için controllers/productAll/index.js dosyası oluşturuldu ve içine

import Product from "../../models/product";

const GetList = async (req, res, next) => {
  try {
    const products = await Product.find({}).sort({ createdAt: -1 });

    res.json(products);
  } catch (e) {
    next(e);
  }
};

export default {
  GetList,
};

                

buradan gelen yönlendirme client tarafında api.js içinde yakalandı.

export const fetchProductAllList = async ({ pageParam = 0 }) => {
  const { data } = await axios.get(
    `${process.env.REACT_APP_BASE_ENDPOINT}/product-all`
  );

  return data;
};
                

Bu fonksiyon pages\Admin\AdminProducts\index.js içinde import edilip fetchProductList yerine kullanıldı.

Admin/Home sayfası düzenlendi.