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.
Web sayfasını oluşturan html etiketlerinin tamamı Real DOM'u oluşturur.
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.
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.
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 {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"
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.
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);
})();
.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)
push, map, find, filter, some, every, includes
Örnek
const users = ["Mehmet", "Ahmet". "Murat"]
users.push("Ayşe")
console.log(users)
çıktı:
["Mehmet", "Ahmet". "Murat", "Ayşe"]
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
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 }
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 }
]
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
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
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
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.
Web sayfasını meydana getiren bileşenlerdir. Bu companentler birleşerek başka kompanentleri ve sonunda web sayfamızı oluştururlar.
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:
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ı.
import './App.css';
import Header, { } from "./components/Header";
function App(){
return (
<div>
<Header />
</div>
);
}
export default App;
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.
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.
İ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ş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 (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.
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"]}
{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.
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.
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.
Gönderilecek propun zorunlu olduğunu belirtmek için:
...
name: PropTypes.string.isRequired,
...
Bir prop için birden fazla veri tipi kabul etmek için:
...
age: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]),
...
Obje olarak gönderilen prop için kullanılabilir.
...
adress: PropTypes.shape({
title: PropTypes.string,
zip: PropTypes.number,
})
...
İ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 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.
App.js App fonksiyonunda daha önceki stateleri eklediğimiz yere:
const [friends, setFriends] = useState(['Ahmet', 'Murat']);
ile array yapıda state eklenir.
<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.
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.
<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
...addressifadesi mevcut address bilgilerinin default olmasını sağlar. Bu sayede yeni tanımlanmayan değerler ilk tanıma bağlı kalır.
...addressolmazsa 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.
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>
)
}
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
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;
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;
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/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.
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
İş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>
);
}
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'>
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.
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} |
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
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 axiosyazı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));
},[])
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ı
<Route path="/about"><About /></Route>
<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.
<Link to="/">Home</Link>
<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;
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.
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;
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/> ... )
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.
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';ile formik bileşenleri import edilir.
import { Formik, Field, Form } from 'formik';
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;
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;
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>
...
...
<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"}
...
<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>
<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>
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;
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;
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>}
Ö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 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.
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.
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.
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;
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.
İç 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);
}, []);
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)
Elimizdeki datanın tüm kompanentlerde kullanılabilmesini sağlar. Contex içindeki dataya herhangi bir kompanentten ulaşıp manipule edebiliriz.
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
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
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;
}
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;
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;
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.
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.
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.
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;
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;
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, 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 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.
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.
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-redisyazı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 iyazı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.
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;
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 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;
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;
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.
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;
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;
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;
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;
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 projectile yeni bir proje oluşturduğumuzda ilk test dosyamız src/App.test.js olarak hazır gelir. terminale
npm testyazarak ç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.
});
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;
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")
})
})
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ı?
})
});
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-libraryile global kurulur ve
create-react-libraryile ç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.
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.
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.
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. 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 deployile çalıştırılır.
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.
site setting>domain management>add custom domain
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
site settings>Build & deploy>Deploy notifications
Ö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.
Deploys ekranında tıklayıp açtığımız deployda permalink veya preview butonu ile daha önceki deploy versiyonlarını görebiliriz.
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.
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/defaultyazarak nginx ayar sayfası açıldı.
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.
Ü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 hazır verildi. Terminalde backend dizinine girilip
npm iile gerekli modülleri kuruldu.
npm i -D nodemonile nodemon kuruldu
npm devile başlatıldı.
Backendin çalışması için mondoDB ve redis yüklü olmalı.
redisin çalışması için terminale:
server-redisyazıyoruz.
src içine .env oluşturup içine:
MONGO_URI=mongodb://localhost:27017tanı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.
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;
.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.
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>
<Route path="/" element={<Home />} />
<Route path="/singin" element={<Singin/>} />
<Route path="/singup" element={<Singup />} />
</Routes>
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;
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.
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
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
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 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.
App.js içinde yönlendirme yapılır:
<Route path="/product/:product_id" element={<ProductDetail />} />
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:4000lokal 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
}
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.
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ş 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>
...
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);
}
);
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ı.
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;
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>
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;
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;
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([]);
Ö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.
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;
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;
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;
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;
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.