React ile proje geliştirirken mecburen terminal'de bazı kodlar yazmamız gerekecek :) İşletim sistemlerine uygun olarak terminallerimizi açmayı öğrenmeyeliz.
- MacOS kullanıyorsanız
command
+space
kombinasyonu ile arama kutusunu açıpterminal
yazarak terminali çalıştırabilirsiniz. - Windows kullanıyorsanız başlatma çubuğuna basıp
CMD
yazarsanız varsayılan terminali açacaktır. Ek olarakPowerShell
de kullanılabilir. Ayrıca harici bir terminal isterseniz CMDER terminalini indirip kurabilirsiniz.
React projesini kurmak için farklı yollar olsada, en çok tercih edilen yollardan bir tanesi olan create-react-app ile kurulum yapacağız.
Kurulum yapmak için ilk olarak bir klasör oluşturun. Bunu elle de oluşturabilirsiniz, mkdir
komutu ile terminalden de oluşturabilirsiniz.
mkdir react-projesi
Daha sonra terminal'de oluşturduğunuz klasörün içine cd
komutu ile giriş yapın.
cd react-projesi
ve şu komutla ilgili klasöre react projenizi kurun.
npx create-react-app .
Not: Eğer
npx
hata veriyorsanpm i -g npx
komutu ile global olarak kurup tekrar deneyin.
Burada .
o an bulunduğunuz dizine kurmanız için gerekli değer. Yani siz app
yazsaydınız ilgili klasörün içine app
adıyla bir klasör açıp buraya kuracaktı.
Kurulum ortalama 3-5 dakika arasında bir zaman alacaktır.
Kurulum başarıyla tamamlandıktan sonra projeyi şu komutla başlatın.
npm start
Artık localhost:3000
adresinden react projenizi canlı olarak görebilirsiniz.
Kurulumu yaptıktan sonra, ilk olarak proje yapımızı inceleyelim. Kurulan projemizde şöyle bir klasör yapısı olmasını bekliyoruz:
├── README.md
├── node_modules/
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
├── reportWebVitals.js
└── setupTests.js
node_modules
- Bu klasör ilgili paketlerin yüklendiği klasördür. Yeni bir paket yüklendiğinde burada yer alacaktır, herhangi bir şekilde bu klasörle işimiz olmayacak.public
- Bu klasörde static olarak tutmak istediğiniz dosyalarınız yer alacak. Projeyi build aldığınızda buradaki dosyalar değiştirilmeden build klasörüne aktarılacaktır.index.html
- Burada#root
id'lidiv
etiketisrc/index.js
altında seçilip render işlemi sağlanıyor. Bu dosyaya font dosyalarınızı çağırabilirsiniz, analytics gibi genel kodları vs. ekleyebilirsiniz.
src
- Projemizin tüm kodları bu klasör altında yer alacak. Geliştirmelerin tamamını buradan yapacağız.index.js
- Bu projemizin başlangıç dosyası. Burada React kütüphanesi ve ihtiyaç olanları yükleyip#root
etiketini seçerekApp()
componentinirender()
ediyoruz. Burayıda genel olarak çok ellemeyeceğiz.App.js
- Genelde temel component'imiz bu olacak. Bunun üzerinden projemizi inşa etmeye başlayacağız ancak bunun adı herhangi bir şeyde olabilirdi. İstediğinizi koymakta özgürsünüz.
.gitignore
- Bir versiyon kontrol sistemi ile çalışırsanız burada git'e gönderilmeyecek dosya ve klasörler belirleniyor.package.json
- 1. günde bu dosyanın ne işe yaradığını ve nasıl kullanıldığını öğrendik. Detaylar için 1. gün özetine bakabilirsiniz.README.md
- Bir versiyon kontrol sistemi ile çalışıyorsanız projenin ne olduğunun anlatıldığı bir markdown dosyasıdır.
Artık klasör yapısıyla ilgili genel bir fikrimiz var. Yavaştan bir şeylere bakmaya hazırız :)
React kodlarında gördüğünüz HTML kodlarının aslında HTML kodu olmadığını biliyor musunuz? Elbette çıktı olarak bunlar nihayetinde HTML etiketlerine dönüştürülüyor ancak bizim yazdıklarımız birer javascript objesinden ibaret. Bu yapının adıda JSX olarak geçiyor.
JSX ile HTML arasında çok fazla fark olmamakla birlikte bazı önemli farklar da mevcut elbette.
- HTML'de
class
olarak tanımlanan nitelik JSX'declassName
olarak tanımlanmalı. - HTML'de tek kelime olmayan
tabindex
gibi nitelikler JSX'decamelCase
yapısına uygun olaraktabIndex
şeklinde dönüştürülür. - HTML'de
onclick
,onchange
gibi nitelikler yukarıdakicamelCase
örneğine göreonClick
,onChange
şeklinde dönüştürülür. - HTML'de
label
etiketinde belirtilenfor
niteliği JSX'dehtmlFor
olarak tanımlanmalı. - HTML'de
value
niteliği JSX'dedefaultValue
olarak tanımlanmalı. - Ayrıca
{}
içinde javascript ifadeleri çalıştırılabilir. Bunamap
,&&
gibi yapılarda dahil. - Ayrıca niteliklere geçilen değerler
nitelil={degisken}
şeklinde kullanılabilir.
Component'leri belli bir amaca hizmet eden birer UI olarak düşünebilirsiniz. Örneğin button, dropdown, tab, form elemanları ve aklınıza gelecek her şey birer component olabileceği gibi react'de render edilen her şey birer component'dir.
Projemiz üstte de bahsettiğimiz gibi bir component'in üzerine inşa edilir. Belki bu kafanızı karıştırabilir, ancak zamanla daha iyi kafanızda oturacaktır.
Component isimleri büyük harfle başlar, çünkü diğer JSX etiketleri ile karıştırılmaması için bu şekilde bir kural belirlenmiştir.
En yalın haliyle bir component şuna benzer:
// ./components/Button.js
function Button() {
return (
<button>buton örneği</button>
)
}
Button
adında bir component geriye ise button
etiketiyle static bir değer döndürüyor.q
Bu component'i bir başka component içinde çağırıp kullanmak isterseniz şöyle bir yol izlemelisiniz:
import Button from "./components/Button"
function App() {
return (
<div>
<h3>Component Kullanımı</h3>
<Button />
<Button>Bu şekilde de kullanılabilir.</Button>
</div>
)
}
Elbette component'in bu hali pek bir şey ifade etmiyor, zira bu component'e bazı bilgiler göndererek component içinde bunlara göre kontroller yapıp geriye bir şeyler döndürmeliyiz.
prop'ları bir fonksiyona gönderilen parametreler gibi düşünebilirsiniz. Component'e gönderilen prop'lar anahtar="değer"
şeklinde gönderilir. Yukarıdaki component'imize bazı proplar göndererek daha anlamlı bir component haline dönüştürelim.
// ./components/Button.js
function Button(props) {
return (
<button className={props.variant}>{props.text}</button>
)
}
Artık bu component'e gönderebileceğimiz text
ve variant
adında iki prop'umuz var. Kullanırken şöyle kullanacağız:
import Button from "./components/Button"
function App() {
return (
<div>
<h3>Component Kullanımı</h3>
<Button text="Buton Örneği" variant="danger" />
</div>
)
}
Burada props
yerine 1. günde öğrendiğimiz destructuring
ile objeleri isimleriyle kullanabiliriz. Şöyle ki:
// ./components/Button.js
function Button({ variant, text }) {
return (
<button className={variant}>{text}</button>
)
}
Ek olarak bu component'e bazı prop'lar opsiyonel olarak geçilebilir. Örneğin disabled
, onClick
, style
gibi prop'lar geçmek isteyebilirsiniz.
Ancak bunları tek tek yazmak yerine, kalanları rest
operatörü yardımıyla alıp spread
operatörü yardımıyla kullanabilirsiniz.
// ./components/Button.js
function Button({ variant, text, ...props }) {
return (
<button {...props} className={variant}>{text}</button>
)
}
Kullanırken de şöyle kullanabilirsiniz:
import Button from "./components/Button"
function App() {
return (
<div>
<h3>Component Kullanımı</h3>
<Button
onClick={() => alert('Butona tıklandı.')}
disabled={true}
style={{ backgroundColor: 'red', color: '#fff' }}
text="Buton Örneği"
variant="danger"
/>
</div>
)
}
Artık bir buton etiketine eklenecek prop'ları düşünmemize gerek yok, biz sadece bizi ilgilendirenleri alıp kalanlarını buton etiketine prop olarak geçebiliriz.
Yukarıdaki buton component'ini şöyle kullanmak isteseydim, etiket içindeki değerleri nasıl alabilirdim?
import Button from "./components/Button"
function App() {
return (
<Button>
bu alan içindeki
değer nasıl alınacak?
</Button>
)
}
Evet, props
altında children
değeri geliyor, bu etiket içindeki değeri temsil ediyor. Bu sayede bir component'in içine farklı JSX elemanlarını ekleyip, children
prop'u ile bunları kolayca yönetebilirsiniz.
function Button({ children, ...props}) {
return (
<button {...props}>
{children}
</button>
)
}
Bazı durumlarda bazı değerleri global olarak ortama göre tutmak isteyebilirsiniz. React ile proje geliştirirken temelde 2 ortamımız olacak. development
ortamı yani geliştirme yaptığımız ortam ve production
ortamı yani ürün seviyesinde ki ortamımız.
Örneğin bir API URL'imiz olduğunu düşünelim, bu değer development ya da production ortamlarında farklı olabilir. Bu gibi değerleri .env
uzantılı dosyalarda tutacağız.
Şu isimlerle .env dosyası oluşturabilirsiniz ve ana dizinde yer almalı:
.env.development
- geliştirme ortamında geçerli ortam değişkenleri.env.production
- production ortamında geçerli ortam değişkenleri
Sırasıyla bu dosyaları ana dizinimizde oluşturalım ve içine 1-2 ortam değişkeni tanımlayalım.
Ortam değişkenlerini tanımlarken ön ek olarak
REACT_APP_
gelmeli ve sonrasında değişken adınız yer almalı.
# .env.development
REACT_APP_API_URL=http://localhost/api
REACT_APP_TITLE=test title
# .env.production
REACT_APP_API_URL=http://test.com/api
REACT_APP_TITLE=production title
Artık bunları component içinde kullanmak istediğimizde şöyle erişip kullanabiliriz.
function App() {
return (
<div>
{process.env.REACT_APP_API_URL} <br />
{process.env.REACT_APP_TITLE}
</div>
)
}
Ayrıca hangi ortamda olduğunuzu anlamak için process.env.NODE_ENV
ile kontrol yapabilirsiniz. Sadece production ortamında bazı kodlarınızı çalıştırmak isteyebilirsiniz. Örn: analytics bilgileriniz gibi.
if (process.env.NODE_ENV === 'production') {
// analytics bilgileri
}
Ayrıca ortam değişkenlerini HTML dosyasında da %REACT_APP_TITLE%
şeklinde kullanabileceğinizi unutmayın.
State kavramını sıkça duyacaksınız. Aslında bir değişken ve onun taşıdığı değer gibi düşünebilirsiniz. Farkı ise, bu değişkenler reaktiftir. Yani bir değişiklik olduğunda react bunu bilir ve kullanılan yerleri günceller, buna bağlı hesaplamalar varsa bunları yeniden hesaplar vs.
Elbette tanımlarken bir değişken gibi tanımlamaktan ziyade react'in bize verdiği useState
hook'unu (metodunu) kullanacağız.
useState
hook'u bize bir iki değerli bir dizi döndürür. İlki state'in değeri, ikincisi ise state'i güncellemek için gerekli fonksiyondur. Biz de destructuring
yardımı ile isimlendirip component'imizin içinde kullanacağız.
Ayrıca useState()
içine bir başlangıç değeri belirleyeceğinizi unutmayın. Örnek kullanımlar:
const [name, setName] = useState('tayfun')
const [tags, setTags] = useState(['html', 'css', 'js'])
const [user, setUser] = useState({
name: 'Tayfun',
surname: 'Erbilen'
})
Gelin bir component içinde basit bir state oluşturalım ve bunu ekrana basalım. Daha sonra buton'a basıldığında bu state'in değerini güncelleyelim.
import { useState } from "react"
function App() {
const [name, setName] = useState('tayfun')
const changeName = () => {
setName('recep')
}
return (
<div>
<button onClick={changeName}>İsmi değiştir</button>
<h3>İsim = {name}</h3>
</div>
)
}
Bir başka örneği de input'dan yazılan değeri state'e atayarak görelim.
import { useState } from "react"
function App() {
const [name, setName] = useState('tayfun')
return (
<div>
<input defaultValue={name} onChange={e => setName(e.target.value)} />
<h3>İsim = {name}</h3>
</div>
)
}
Component'lerde return işleminden sonra bir kod varsa bunlar çalışmayacaktır. Bu yüzdenk koşullu olarakta render yapabilirsiniz. Örneğin kullanıcı giriş yapmışsa başka bir değer, yapmamışsa başka bir değer return edebilirdik.
function App() {
const isLoggedIn = true;
if (isLoggedIn) {
return (
<div>
<h3>Hoşgeldiniz</h3>
</div>
)
}
return (
<div>
<button onClick={() => setShow(show => !show)}>
{show ? 'Gizle' : 'Göster'}
</button>
{show && (
<div>bu alan show state değeri true olduğunda görünür!</div>
)}
</div>
)
}
Ayrıca return içinde de duruma göre göstermek istediğimiz alanları &&
ile belirleyip kullanabilirdik.
function App() {
const [show, setShow] = useState(false)
return (
<div>
<button onClick={() => setShow(show => !show)}>
{show ? 'Gizle' : 'Göster'}
</button>
{show && (
<div>bu alan show state değeri true olduğunda görünür!</div>
)}
</div>
)
}
Bir dizi değerinizi map()
metodu yardımıyla kullanabilirsiniz. Örneğin basit bir todo uygulaması yapalım, input'dan bir değer alalım ve butona basınca todos state'ini güncelleyip bu yeni değeride diziye dahil edelim. Ve diziyi map()
yardımı ile listeleyelim.
import { useState } from "react"
function App() {
const [todo, setTodo] = useState('')
const [todos, setTodos] = useState([])
const addTodo = () => {
//setTodos([...todos, todo])
// yukarıdaki ya da aşağıdaki gibi tanımlanabilir
setTodos(todos => [...todos, todo])
}
return (
<div>
<input defaultValue={todo} onChange={e => setTodo(e.target.value)} />
<button disabled={!todo} onClick={addTodo}></button>
<ul>
{todos.map(todo => (
<li>{todo}</li>
))}
</ul>
</div>
)
}
Tekrarlanan işlemlerde react bizden tekrarlanan elemanlara key
propu geçmemizi bekler. Bu prop'un değeri tekrarlanan işlem içinde benzersiz olmalıdır.
Genelde son çare olarak index değerini key olarak belirlemeniz beklenir. Dizi öğelerinizin değişme ihtimali varsa (örneğin dizinin sonuna başına ortasına yeni değerler geldi) bu gibi durumlarda index'i kullanmamamız tavsiye ediliyor. Şimdi nasıl kullanacağız bir bakalım.
Todo örneğindeki şu kısmı:
<ul>
{todos.map(todo => (
<li>{todo}</li>
))}
</ul>
şöyle değiştirelim:
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
Component'lere geçilen prop'ların daha kontrollü olması için prop-types
paketini kurup proplarımızın tiplerini belirtebiliriz. Genelde react projelerini bir takım halinde yazacağımız için sizden sonra gelenlerinde component'leri daha rahat kullanması ve daha anlamlı olması için bu işlemi yapmanız fena olmaz.
Paketi kurmak için:
npm i prop-types
Daha sonra bir component'de şu şekilde işlem yapmanız gerekir:
// ./components/Button.js
import PropTypes from "prop-types"
function Button({ variant, text, ...props }) {
return (
<button {...props} className={variant}>{text}</button>
)
}
Button.propTypes = {
variant: PropTypes.string.isRequired,
text: PropTypes.string.isRequired
}
export defaul Button
Kısaca kullanabilecekleriniz:
- PropTypes.array - diziler için
- PropTypes.bool - mantıksal değerler için
- PropTypes.func - fonksiyonlar için
- PropTypes.number - sayılar için
- PropTypes.object = nesneler için
- PropTypes.string - ifadeler için
- PropTypes.any - herhangi birisi için
- PropTypes.element - bir JSX elemanı için
- PropTypes.oneOf(['success', 'danger']) - bazı durumlarda sadece belirtilen değerlerin olduğundan emin olmak için
- PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]) - birden fazla tip uygulamak için
- PropTypes.arrayOf(PropTypes.number) - bir dizi içindeki tipler için
- PropTypes.objectOf(PropTypes.string) - bir nesne içindeki tipler için
- PropTypes.shape({ name: PropTypes.string, age: PropTypes.number }) - Obje içindeki tipleri belirtmek için
Not:
isRequired
değerini yukarıdaki herhangi bir tanımın sonuna ekleyip zorunlu olduğunu belirtebilirsiniz.
Ayrıca defaultProps
ile component'e gönderilen prop'ların varsayılan değerlerini belirleyebiliyoruz.
// ./components/Button.js
import PropTypes from "prop-types"
function Button({ variant, text, ...props }) {
return (
<button {...props} className={variant}>{text}</button>
)
}
Button.propTypes = {
variant: PropTypes.string.isRequired,
text: PropTypes.string.isRequired
}
Button.defaultProps = {
variant: 'default'
}
export defaul Button
React'de en çok kullanacağımız şeylerden biride form elemanları, ve bunları nasıl yöneteceğimiz konusu. Gelin tek tek form elemanlarını nasıl yönetebiliriz inceleyelim.
Tipi checkbox ya da radio olmadığı sürece herhangi bir input türü şöyle kullanılabilir:
import React, { useState } from 'react';
export default function App() {
const [name, setName] = useState('tayfun');
const [surname, setSurname] = useState('erbilen');
return (
<div>
<button onClick={() => setName('Recep')}>Adı Değiştir</button>
<button onClick={() => setSurname('Durmuş')}>Soyadı Değiştir</button>
<hr />
<input
type="text"
defaultValue={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="text"
value={surname}
onChange={(e) => setSurname(e.target.value)}
/>
<hr />
Ad = {name} <br />
Soyad = {surname}
</div>
);
}
HTML'de <textarea>açıklama</textarea>
şeklinde tanımlansada react'de value
prop'u ile değeri belirtilerek kullanılabilir.
import React, { useState } from 'react';
export default function App() {
const [description, setDescription] = useState('örnek description');
return (
<div>
<button onClick={() => setDescription('bu bir test açıkalamadır')}>
Açıklamayı Değiştir
</button>
<hr />
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<hr />
Açıklama = {description}
</div>
);
}
Select'de de yine value
ile değer belirlenir.
import React, { useState } from 'react';
export default function App() {
const [category, setCategory] = useState();
return (
<div>
<button onClick={() => setCategory(3)}>Kategoriyi Değiştir</button>
<hr />
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option>- Seçin -</option>
<option value="1">PHP</option>
<option value="2">CSS</option>
<option value="3">HTML</option>
<option value="4">JavaScript</option>
</select>
<hr />
Seçilen Kategori = {category}
</div>
);
}
Ayrıca seçilen kategori'nin adını getirmek isteseydik şöyle de kullanabilirdik:
import React, { useState } from 'react';
const categories = ['PHP', 'CSS', 'HTML', 'JavaScript'];
export default function App() {
const [category, setCategory] = useState();
return (
<div>
<button onClick={() => setCategory(3)}>Kategoriyi Değiştir</button>
<hr />
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option>- Seçin -</option>
{categories.map((category, index) => (
<option value={index} key={index}>{category}</option>
))}
</select>
<hr />
Seçilen Kategori = {categories[category] || 'Seçilmedi'}
</div>
);
}
Ya da obje olarak şöyle kullanabilirdik:
import React, { useState } from 'react';
const categories = [
{ id: 1, name: 'PHP' },
{ id: 2, name: 'CSS' },
{ id: 3, name: 'HTML' },
{ id: 4, name: 'JavaScript' },
];
export default function App() {
const [category, setCategory] = useState();
return (
<div>
<button onClick={() => setCategory(3)}>Kategoriyi Değiştir</button>
<hr />
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option>- Seçin -</option>
{categories.map((category) => (
<option value={category.id} key={category.id}>
{category.name}
</option>
))}
</select>
<hr />
Seçilen Kategori ={' '}
{categories[category]
? JSON.stringify(categories[category])
: 'Seçilmedi'}
</div>
);
}
Multiple selectbox örneğimiz ise şöyle olurdu:
import React, { useState } from 'react';
const categoryList = [
{ id: 1, name: 'PHP' },
{ id: 2, name: 'CSS' },
{ id: 3, name: 'HTML' },
{ id: 4, name: 'JavaScript' },
];
export default function App() {
const [categories, setCategories] = useState();
const getCategories =
categories && categoryList.filter((c) => categories.includes(c.id));
return (
<div>
<button onClick={() => setCategories([2, 3])}>
Kategorileri Değiştir
</button>
<hr />
<select
multiple={true}
value={categories}
onChange={(e) =>
setCategories(
[...e.target.selectedOptions].map((option) => Number(option.value))
)
}
>
<option>- Seçin -</option>
{categoryList.map((category) => (
<option value={category.id} key={category.id}>
{category.name}
</option>
))}
</select>
<hr />
Seçilen Kategori = {categories ? JSON.stringify(categories) : 'Seçilmedi'}
<hr />
<pre>{JSON.stringify(getCategories, null, 2)}</pre>
</div>
);
}
import React, { useState } from 'react';
export default function App() {
const [checked, setChecked] = useState(false);
return (
<div>
<button onClick={() => setChecked((checked) => !checked)}>
Checkboxı Değiştir
</button>
<hr />
<label>
<input
type="checkbox"
value={checked}
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
Kuralları kabul ediyorum
</label>
<hr />
Kurallar kabul edildi = {checked.toString()}
</div>
);
}
Birden fazla checkbox'ı yönetmek isteseydik:
import React, { useState } from 'react';
export default function App() {
const [rules, setRules] = useState([
{ id: 1, label: 'Kuralları kabul ediyorum', checked: false },
{ id: 2, label: 'Sözleşmeyi kabul ediyorum', checked: false },
{ id: 3, label: 'Kullanım koşullarını kabul ediyorum', checked: false },
]);
const changeHandle = (e) => {
setRules((rules) =>
rules.map((r) => {
if (r.id == e.target.value) {
r.checked = e.target.checked;
}
return r;
})
);
};
const acceptTogggle = () => {
setRules((rules) =>
rules.map((rule) => ({
...rule,
checked: !rule.checked,
}))
);
};
const disabled = rules.every((rule) => rule.checked);
return (
<div>
<button onClick={acceptTogggle}>Tümünü Kabul Et</button>
<hr />
{rules.map((rule) => (
<label style={{ display: 'block' }} key={rule.id}>
<input
type="checkbox"
value={rule.id}
checked={rule.checked}
onChange={(e) => changeHandle(e)}
/>
{rule.label}
</label>
))}
<hr />
<button disabled={!disabled}>Devam et</button>
<hr />
<pre>{JSON.stringify(rules, null, 2)}</pre>
</div>
);
}
import React, { useState } from 'react';
const options = [
{ option: 'Option 1', id: 1 },
{ option: 'Option 2', id: 2 },
{ option: 'Option 3', id: 3 },
];
export default function App() {
const [selected, setSelected] = useState(false);
const option = options.find((o) => o.id === selected);
return (
<div>
<button onClick={() => setSelected(2)}>Option'ı Değiştir</button>
<hr />
{options.map((option) => (
<label style={{ display: 'block' }} key={option.id}>
<input
type="radio"
value={option.id}
checked={selected === option.id}
onChange={(e) => setSelected(option.id)}
/>
{option.option}
</label>
))}
<hr />
<pre>{option ? JSON.stringify(option) : 'Seçilmedi'}</pre>
</div>
);
}
Dosyalarla çalışırken biraz daha farklı yaklaşıyoruz:
import React, { useState } from 'react';
export default function App() {
const [file, setFile] = useState(false);
const changeHandle = (e) => {
setFile(e.target.files[0]);
};
return (
<div>
<label>
<input type="file" onChange={changeHandle} />
</label>
{file && <div>Seçilen dosya = {file.name}</div>}
</div>
);
}
Seçilen bir görseli önizlemek için:
import React, { useState } from 'react';
export default function App() {
const [file, setFile] = useState(false);
const changeHandle = (e) => {
if (e.target.files[0]) {
const fileReader = new FileReader();
fileReader.addEventListener('load', function () {
setFile(this.result);
});
fileReader.readAsDataURL(e.target.files[0]);
} else {
setFile(false);
}
};
return (
<div>
<label>
<input type="file" accept="image/*" onChange={changeHandle} />
</label>
{file && <img src={file} />}
</div>
);
}
Birden fazla dosya seçimi için:
import React, { useState } from 'react';
export default function App() {
const [files, setFiles] = useState(false);
const changeHandle = (e) => {
setFiles([...e.target.files]);
};
return (
<div>
<label>
<input type="file" multiple={true} onChange={changeHandle} />
</label>
{files && files.map((file) => <div>{file.name}</div>)}
</div>
);
}
Bir component'in yaşam döngüsü vardır. Bu component ilk yüklendiğinde, component render edildiğinde, component yok edildiğinde çalışacak kodları belirlememizi sağlar.
Bunun için useEffect()
hook'u kullanılır. İlk parametre bir callback fonksiyonu, ikinci parametre ise yukarıdaki durumlara göre belirlenir.
Eğer 2. parametre olarak boş bir array verilirse, component ilk yüklendiğinde çalışması sağlanır.
import { useEffect } from "react"
function App() {
useEffect(() => {
// component ilk yüklendiğinde burası çalışır
}, [])
}
Eğer 2. parametre hiç verilmezse component her render ediliğinde çalışır.
import { useEffect } from "react"
function App() {
useEffect(() => {
// component her render edildiğinde burası çalışır
})
}
Eğer 2. parametre boş bir array verilirse ve geriye bir fonksiyon return edilirse component yok ediliğinde çalışması sağlanır.
import { useEffect } from "react"
function App() {
useEffect(() => {
// component ilk yüklendiğinde burası çalışır
return () => {
// component yok edildiğinde burası çalışır
}
}, [])
}
Ayrıca state'lerde component'in bir parçası olduğu için, bunların değişimini de yine 2. parametre'de dizi içinde belirterek kullanabilirsiniz.
import { useState, useEffect } from "react"
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('count değeri değişti', count)
}, [count])
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Değer Artır</button>
<button onClick={() => setCount(c => c - 1)}>Değer Azalt</button>
<p>counter değeri = {counter}</p>
</div>
)
}
birden fazla state'i tek bir
useEffect
de takip etmek istereseniz 2. parametreyi[state1, state2]
şeklinde belirleyebilirsiniz.
React varsayılan olarak geriye bir JSX elemanı return etmenizi bekler. Yani birkaç tane JSX elemanını her zaman bir elemana sarmalayıp öyle döndürmenizi bekler. Ancak bazı durumlarda sarmalama işlemini bir etiketle yapmak yerine boş olarak ayarlamak isteyebilirsiniz. İşte bu gibi durumlarda Fragment
kullanılır.
function App() {
return (
<h3>Başlık</h3>
<p>Paragraf</p>
<button>buton örneği</button>
)
}
Yukarıdaki hatalı bir kullanımdır, bu üç elemanı sarmalayıp tek bir eleman return etmeliyiz.
function App() {
return (
<div>
<h3>Başlık</h3>
<p>Paragraf</p>
<button>buton örneği</button>
</div>
)
}
Bu kullanım doğru olsada gereksiz yere div
etiketi içinde yer alacaktır kodlarımız. Bunun yerine Fragment'i kullanalım birde.
import { Fragment } from "react"
function App() {
return (
<Fragment>
<h3>Başlık</h3>
<p>Paragraf</p>
<button>buton örneği</button>
</Fragment>
)
}
Artık gereksiz bir etiketin içinde değiller ve yine başarıyla kodlarımız çalışıyor. Ayrıca Fragment
import etmeden <>
ile kısa kullanımınıda kullanabiliriz.
function App() {
return (
<>
<h3>Başlık</h3>
<p>Paragraf</p>
<button>buton örneği</button>
</>
)
}
React'de JSX kullandığımızı söylemiştim. JSX ise kodları aslında birer javascript objesine çevriliyor. createElement()
ile bir JSX objesi oluşturabiliriz. Yani örneğin şöyle bir JSX kodunu:
<button className="btn" onClick={() => console.log('tıkladın!')}>
Button
</button>
şöyle yazabilirdik:
import { createElement } from "react"
const button = createElement('button', {
className: 'btn',
onClick: () => console.log('tıkladın!')
}, 'Button')
bir başka örnekte şöyle olabilir:
function App() {
const todos = ['todo 1', 'todo 2', 'todo 3']
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
)
}
bu da şu şekilde yazılabilirdi:
import { createElement } from "react"
function App() {
const todos = ['todo 1', 'todo 2', 'todo 3']
const ul = createElement('ul', null, todos.map((todo, index) => createElement('li', {
key: index
}, todo)))
return ul
}
Bazı durumlarda react elemanlarına javascript tarafında erişip işlem yaptırmak isteyebilirsiniz. Bu gibi durumlarda o elemanı ref'lemek gerekiyor ve bunun içinde useRef()
hook'u kullanılıyor.
Örneğin bir input'umuz olsun ve bir button'a basınca bu input'a focuslamak istediğimizi varsayalım. Input'a focuslamak için evvela input'a erişebilmemiz lazım bunun içinde useRef()
ile refleme işlemi yapabiliriz.
import { useRef } from "react"
function App() {
const inputRef = useRef()
return (
<>
<input type="text" ref={inputRef} />
<button onClick={() => {
inputRef.current.focus()
}}>Focusla</button>
</>
)
}
Gördüğünüz gibi dom elemanına ulaşmak için inputRef.current
şeklinde bir kullanım gerçekleştirdik.
Yukarıdaki senaryomuz aynı kalsaydı ve input etiketi yerine bu bir component olsaydı, bu işlemi forwardRef()
ile ref'i ileterek yapmak zorundaydık. Yani:
import { useRef } from "react"
const Input = forwardRef((props, ref) => {
return <input type="text" {...props} ref={ref} />;
});
export default function App() {
const inputRef = useRef();
return (
<>
<Input ref={inputRef} />
<button
onClick={() => {
inputRef.current.focus();
}}
>
Focusla
</button>
</>
);
}
useState() ile yönetmekte zorlandığınız daha karmaşık yapılarınızda useReducer()
kullanabilirsiniz. Örnek bir counter uygulamasından durumu açıklayalım.
import {useState} from "react";
function Todos() {
const [todos, setTodos] = useState([])
const [todo, setTodo] = useState('')
const addTodo = () => {
setTodos([...todos, todo])
setTodo('')
}
const deleteTodo = index => {
setTodos([...todos.filter((t, i) => i !== index)])
}
return (
<>
<input type="text" value={todo} onChange={e => setTodo(e.target.value)}/>
<button disabled={!todo} onClick={addTodo}>Ekle</button>
<ul>
{todos.map((todo, index) => (
<li onClick={() => deleteTodo(index)} key={index}>{todo}</li>
))}
</ul>
</>
)
}
export default Todos
Yukarıdaki örnek basit bir todo örneği, şimdi bunu useReducer()
ile yeniden düzenleyelim.
import {useReducer} from "react";
function reducer(state, action) {
switch (action.type) {
case 'UPDATE_TODO':
return {
...state,
todo: action.value
}
case 'ADD_TODO':
return {
...state,
todo: '',
todos: [...state.todos, action.todo]
}
case 'DELETE_TODO':
return {
...state,
todos: [...state.todos.filter((t, i) => i !== action.index)]
}
}
}
function Todos() {
const [state, dispatch] = useReducer(reducer, {
todos: [],
todo: ''
})
const addTodo = () => {
dispatch({ type: 'ADD_TODO', todo: state.todo })
}
const deleteTodo = index => {
dispatch({ type: 'DELETE_TODO', index })
}
return (
<>
<input type="text" value={state.todo} onChange={e => dispatch({ type: 'UPDATE_TODO', value: e.target.value })}/>
<button disabled={!state.todo} onClick={addTodo}>Ekle</button>
<ul>
{state.todos.map((todo, index) => (
<li onClick={() => deleteTodo(index)} key={index}>{todo}</li>
))}
</ul>
</>
)
}
export default Todos
Artık state'ler useRecuder()
altında dönen değerlerden okunuyor. Bir işlem yaptırmak içinde dispatch()
metoduna type
ve gerekli değerleri geçerek tanımlıyoruz. Reducer fonksiyonunu da başka bir dosyada tutup daha düzenli bir kod yapısı oluşturabilirsiniz.
Memoization, bir optimizasyon tekniğidir. Primitive (ilkel) veri türlerinde 2 veriyi değerlerine göre karşılaştırmak kolaydır. Örneğin:
"a" === "a" // true
true === true // true
34 === 34 // true
Ancak iş non-primitive (ilkel olmayan) veri türlerine geldiğinde bu sefer değerleri değil referansları karşılaştırılır. Yani şu örnekler birbirine eşit değildir:
{} === {}
[] === []
Görünürde bir farkları olmasada bellekte tutuldukları referansları farklı olduğu için hiçbir zaman aynı olmayacaklar. İşte react'de bazı metodlar yardımıyla bu non-primitive türleri saklayıp, değişmediği taktirde bunu saklandığı yerden döndürerek tekrar bir maliyet çıkarmadan işlemimizi daha performanslı sürdürebiliriz.
Component'lerin gereksiz yere render edilmemesi için kullanılır. Örneğin bir component'in içinde başka bir component'i çağırdık. Eğer üst component'imiz render olursa, çağırdığımız çocuk component'de render olacaktır. En sık göreceğiniz örneği ise şöyledir:
// ./components/Header.js
function Header({ text }) {
console.log('header component render edildi')
return (
<header>{text}</header>
)
}
export default Header
import { useState } from "react"
import Header from "./components/Header"
function App() {
console.log('App component render edildi')
const [count, setCount] = useState(0)
const [text, setText] = useState('header')
return (
<>
<Header text={text} />
<h3>{count}</h3>
<button onClick={() => setCount(c => c + 1)}>Artır</button>
<button onClick={() => setText('yeni header yazısı')}>Header Yazısını Değiştir</button>
</>
)
}
Yukarıdaki örnekte Artır butonuna her bastığımızda konsol'da header component render edildi'
mesajını göreceğiz. İşte bunu önlemek için Header
component'ini export ederken memo()
içinde export edebilirdik.
Yani şunun yerine:
export default Header
şu şekilde kullanmalıydık:
import { memo } from "react"
// ..
export default memo(Header)
Arıtk App
component'i render olsa bile Header
component'i sadece ona geçilen prop'lar değişirse yeniden render olacaktır. Bunu anlamak için header yazını değiştirme butonuna basıp test edebilirsiniz. 2. kez basıldığında state öncekiyle aynı olduğu için component yine render edilmeyecektir.
Ayrıca iç içe kullanımlarda da bu tekniği kullanmak faydalı olacaktır. Bir todo örneğinden yola çıkacak olursak:
// ./AddTodo.js
import { useState, memo } from 'react';
function AddTodo({ setTodos }) {
console.log('add todo');
const [todo, setTodo] = useState('');
const submitHandle = (e) => {
e.preventDefault();
setTodos((todos) => [...todos, todo]);
setTodo('');
};
return (
<form onSubmit={submitHandle}>
<input
type="text"
value={todo}
onChange={(e) => setTodo(e.target.value)}
/>
<button disabled={!todo} type="submit">
Ekle
</button>
</form>
);
}
export default memo(AddTodo);
// ./TodoList.js
import { memo } from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos }) {
console.log('todo list');
return (
<ul>
{todos.map((todo, index) => (
<TodoItem todo={todo} key={index} />
))}
</ul>
);
}
export default memo(TodoList);
// ./TodoItem.js
import { memo } from 'react';
function TodoItem({ todo }) {
console.log('todo item', todo);
return <li>{todo}</li>;
}
export default memo(TodoItem);
import { useState } from 'react';
import AddTodo from './AddTodo';
import TodoList from './TodoList';
export default function App() {
const [todos, setTodos] = useState([]);
const [count, setCount] = useState(0);
return (
<>
<h3>{count}</h3>
<button onClick={() => setCount((c) => c + 1)}>Artır</button>
<AddTodo setTodos={setTodos} />
<TodoList todos={todos} />
</>
);
}
Bu örnekte memo()
metodunu kaldırarak konsol'dan neler olduğunu inceleyip durumu daha iyi anlayabilirsiniz :)
Component render olduğunda çocuk component'e prop olarak geçilen değerler non-primite (ilkel olmayan) değerler ise çocuk component'de yeniden render olacak. Yukarıdaki örneğimizde todos
değeri yerine filteredTodos
gibi bir değeri prop olarak geçiyor olsaydık, yine count
artırınca App
render olacağı için ve filteredTodos
artık eski referansına sahip olmayacağı için prop değişmiş gibi görünecek ve çocuk component'de render olacaktır. İşte bu gibi durumlarda bunu useMemo()
ile optimize edebiliriz. Böylece component render olsa bile refransı değişmez, sadece gerçekten bir değişiklik olduğunda çocuk component'i render eder.
Örnek olarak yukarıdaki App
component'ine bir search input'u ekleyip todo'ları filtreleme işlemi yapalım. Ve TodoList
e prop olarak todos
değilde filteredTodos
propunu geçelim.
import { useState } from 'react';
import AddTodo from './AddTodo';
import TodoList from './TodoList';
export default function App() {
const [todos, setTodos] = useState([]);
const [count, setCount] = useState(0);
const [search, setSearch] = useState('')
const filteredTodos = todos.filter(todo => todo.toLocaleLowerCase().includes(search.toLocaleLowerCase()))
return (
<>
<h3>{count}</h3>
<button onClick={() => setCount((c) => c + 1)}>Artır</button>
<hr />
<input type="text" placeholder="Todolarda ara" value={search} onChange={e => setSearch(e.target.value)} />
<hr />
<AddTodo setTodos={setTodos} />
<TodoList todos={filteredTodos} />
</>
);
}
Şimdi burada eğer Artır
butonuna basarsak TodoList
in memo()
kullanmamıza rağmen render olduğunu görüyoruz. Yukarıda açıkladığım sebeplerden dolayı, bu yüzden filteredTodos
değerini useMemo()
ile kullanmamız lazım. Yani:
import { useState, useMemo } from 'react';
import AddTodo from './AddTodo';
import TodoList from './TodoList';
export default function App() {
const [todos, setTodos] = useState([]);
const [count, setCount] = useState(0);
const [search, setSearch] = useState('')
const filteredTodos = useMemo(() => {
return todos.filter(todo => todo.toLocaleLowerCase().includes(search.toLocaleLowerCase()))
}, [todos, search])
return (
<>
<h3>{count}</h3>
<button onClick={() => setCount((c) => c + 1)}>Artır</button>
<hr />
<input type="text" placeholder="Todolarda ara" value={search} onChange={e => setSearch(e.target.value)} />
<hr />
<AddTodo setTodos={setTodos} />
<TodoList todos={filteredTodos} />
</>
);
}
useMemo()
ilk parametresi callback fonksiyonu 2. parametresi ise bağımlılıkları yani hangi değerler değiştiğinde yeniden hesaplanması gerektiği 2. parametrede dizi olarak belirleniyor. Artık tekrar aynı işlemi denerseniz render olmadığını göreceksiniz.
Yine component'e prop olarak geçilen metodların üst component render olduğunda yeniden hesaplanıp prop geçilen component'i de render etmemesi için metodlarda useCallback()
kullanılır. Örneğin yukarıdaki örneğimizin devamı olarak aranan kelimeyi silen bir butonu component olarak hazırlayalım ve tıklanınca çalışacak metodu App
component'inden prop olarak geçelim.
// ./ClearButton.js
import { memo } from "react"
function ClearButton({ onClick }) {
console.log('clear button rendered')
return <button onClick={onClick}>Temizle</button>
}
export default memo(ClearButton)
import { useState, useMemo, useCallback } from 'react';
import AddTodo from './AddTodo';
import TodoList from './TodoList';
import ClearButton from './ClearButton';
export default function App() {
const [todos, setTodos] = useState([]);
const [count, setCount] = useState(0);
const [search, setSearch] = useState('')
const filteredTodos = useMemo(() => {
return todos.filter(todo => todo.toLocaleLowerCase().includes(search.toLocaleLowerCase()))
}, [todos, search])
condt clearSearch = useCallback(() => {
setSearch('')
})
return (
<>
<h3>{count}</h3>
<button onClick={() => setCount((c) => c + 1)}>Artır</button>
<hr />
<input type="text" placeholder="Todolarda ara" value={search} onChange={e => setSearch(e.target.value)} />
<ClearButton onClick={clearSearch} />
<hr />
<AddTodo setTodos={setTodos} />
<TodoList todos={filteredTodos} />
</>
);
}
Tipik bir react uygulamasında state'ler üst component'den alt component'lere iletilerek kullanılır. Fakat bu tarz bir kullanım, bire süre sonra component ve state'lerin artmasıyla büyük bir sorun haline gelir.
İşte bu gibi durumlar için state'leri global yönetmek adına react'in bize sağladığı context
yapısını kullanabiliriz.
Global state yönetim araçları elbette var, ancak bu react'in kendisinde bulunan bir yapı olduğu için bilmekte fayda var.
Context yapısını genelde kullanıcı işlemlerinde, tema ve dil gibi global işlemler için kullanabiliriz.
createContext()
ile kullanmak üzere bir context yapısı oluşturulur. Örneğin sitenin dilini ve temasını tutan bir context yapısı oluşturalım ve bunu context/
klasörü içinde SiteContext.js
adıyla oluşturalım.
// ./context/SiteContext.js
import { createContext } from "react"
const SiteContext = createContext()
export default SiteContext
Context yapısını kullanırken diğer tüm kodlarımızı kapsayacak şekilde sarmalamak gerekir.
import Header from "./components/Header"
import SiteContext from "./context/SiteContext"
function App() {
const data = {
theme: 'light',
language: 'tr'
}
return (
<SiteContext.Provider value={data}>
<Header />
</SiteContext.Provider>
)
}
SiteContext.Provider
dedikten sonra value
prop'una paylaşılacak değerler geçilir. Ve örneğin Header
component'inde buna erişmek istediğimizde:
// ./components/Header.js
import { useContext } from "react"
import SiteContext from "../context/SiteContext"
export default function Header() {
const { theme, language } = useContext(SiteContext)
return (
<header>
Site dili: {language} <br />
Site teması: {theme}
</header>
)
}
useContext()
içine SiteContext
belirlenir ve objeden destructuring yardımı ile değerler çıkartılıp kullanılır. Artık component'e prop geçmeden de global olarak değerlerimizi çekmeyi başardık. Ancak hala bir eksik var, Header component'inden bu değerleri değiştiremiyoruz. O zaman App
componentimizde değerleri useState()
ile oluşturup bir de öyle deneyelim.
import { useState } from "react"
import Header from "./components/Header"
import SiteContext from "./context/SiteContext"
function App() {
const [theme, setTheme] = useState('light')
const [language, setLanguage] = useState('tr')
const data = {
theme,
setTheme,
language,
setLanguage
}
return (
<SiteContext.Provider value={data}>
<Header />
</SiteContext.Provider>
)
}
Artık state'i ve onu güncellemek için gerekli fonksiyonlarda global olarak paylaştığım datanın içerisinde. Şimdi Header
component'ini şöyle güncelleyebiliriz.
// ./components/Header.js
import { useContext } from "react"
import SiteContext from "../context/SiteContext"
export default function Header() {
const { theme, setTheme, language, setLanguage } = useContext(SiteContext)
return (
<header>
Site dili: {language} <br />
<button onClick={() => setLanguage('en')}>Dili Değiştir</button>
<hr />
Site teması: {theme} <br />
<button onClick={() => setTheme(theme => theme === 'light' ? 'dark' : 'light')}>Dili Değiştir</button>
</header>
)
}
Artık header component'inden değişikliği yaptık, aynı yapıyı kullanarak diğer bütün component'lerde bu değere erişip kullanabiliriz ve bir değişiklikte bunlar otomatik olarak güncellenir.
Böyle çok karmaşık gibi gelmedi mi? Context'i biraz daha tek dosyada tutup mümkün olduğunca basitleştirmeye çalışalım. İlk olarak context yapımızı şöyle değiştireceğiz:
// ./context/SiteContext.js
import { createContext, useState } from "react"
const SiteContext = createContext()
const SiteProvider = ({ children }) => {
const [theme, setTheme] = useState('light')
const [language, setLanguage] = useState('en')
const data = {
theme,
setTheme,
language,
setLanguage
}
return (
<SiteContext.Provider value={data}>
{children}
</SiteContext.Provider>
)
}
export const useSite = () => useContext(SiteContext)
export default SiteProvider
Artık App
componentimiz şöyle olacak:
import { useState } from "react"
import Header from "./components/Header"
import SiteProvider from "./context/SiteContext"
function App() {
return (
<SiteProvider>
<Header />
</SiteProvider>
)
}
Ve Header
componentimizde şöyle kullanabileceğiz:
// ./components/Header.js
import { useSite } from "../context/SiteContext"
export default function Header() {
const { theme, setTheme, language, setLanguage } = useSite()
return (
<header>
Site dili: {language} <br />
<button onClick={() => setLanguage('en')}>Dili Değiştir</button>
<hr />
Site teması: {theme} <br />
<button onClick={() => setTheme(theme => theme === 'light' ? 'dark' : 'light')}>Dili Değiştir</button>
</header>
)
}
Yan etkileri de context dosyamız içinde kolayca yönetebiliriz. Örneğin temayı ve dili değiştirdiğimizde bunları localStorage'da tutalım, sayfayı yenilediğimizde en son belirlediğimiz tema ve dil hangisi ise onlar kalmaya devam etsin.
// ./context/SiteContext.js
import { createContext, useState, useEffect } from "react"
const SiteContext = createContext()
const SiteProvider = ({ children }) => {
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light')
const [language, setLanguage] = useState(localStorage.getItem('language') || 'en')
const data = {
theme,
setTheme,
language,
setLanguage
}
useEffect(() => {
localStorage.setItem('theme', theme)
localStorage.setItem('language', language)
}, [theme, language])
return (
<SiteContext.Provider value={data}>
{children}
</SiteContext.Provider>
)
}
export const useSite = () => useContext(SiteContext)
export default SiteProvider
state'lerin başlangıç değerlerini eğer varsa localStorage
nesnesinden okuduk. Ve useEffect
ile theme
ya da language
değiştiğinde bunu localStorage'a kaydettik. Böylece bir sonraki yenileyişte değişiklikleri hatırlayacaktır.
Ayrıca birden fazla context yapımız olduğunda bunları her seferinde App
de çağırmayalım ya da state'leri kullanmak istediğimizde farklı dosyalardan çekmeyeli onun yerine context
klasörüne bir index.js
dosyası oluşturup tümünü burada yapalım.
import SiteProvider, { useSite } from "./SiteContext"
export default function Provider({ children }) {
return <SiteProvider>{children}</SiteProvider>
}
export {
useSite
}
Artık App'de kullanırken:
import { useState } from "react"
import Header from "./components/Header"
import Provider from "./context"
function App() {
return (
<Provider>
<Header />
</Provider>
)
}
Ve Header
componentinde erişirken:
// ./components/Header.js
import { useSite } from "../context"
// diğer kodlar...
Böylece bir başka context yapımız olduğunda bunu context/index.js
de çağırıp kullanıcaz ve istediğimiz her yerde onun custom hook'u ile datalarına erişip müdehale edebileceğiz.