Redux bir state yönetim aracıdır. State, uygulamanın o anda tuttuğu verilerdir. State değiştiğinde görünüm de değişir.
Redux stateleri kompanentlerden ayrı bir yerde tutar ve istenildiğinde tüm kompanentlere sunabilir.
Kolay test edilebilir. Kalıpları var bu nedenle de ekip çalışmasına uygun
Çok fazla kavram barındırıyor. Öğrenmesi daha zor. Çok fazla kod yazılıyor.
Primitif veriler birbirine eşitlendiğinde iki ayrı veri olarak saklanırla. biri değiştiğinde diğeri etkilenmez
let name = "Murat"
let name2 = name
olarak tanımladığımızda name veya name2 değişkenini kodun devamında değiştirdiğimizde sadece kendisi değişir. Diğeri değişmez. Primitif veriler (Immutable): undefined, Boolen, Number, String, BigInt, Symbol, null
Referans tipli veriler birbirine eşitlendiğinde bellekte iki ayrı veri olarak değil tek bir veri olarak saklanırlar. Biri değiştiğinde diğerleri de değişir.
let user = {
name: "Murat",
isActive: "true"
}
let user2 = user
olarak tanımladığımızda user veya user2 de değişiklik yaptığımızda diğeri de değişir. Bu durumdan kurtulmak için klonu alınır:
let user = {
name: "Murat",
isActive: "true"
}
let user2 = {...user}
veya obje birleştirme kullanılır:
let user = {
name: "Murat",
isActive: "true"
}
let user2 = Object.assign({}, user) // boş obje ile user'ı birleştirip yeni bir obje yarattık.
Referans tipi veriler (Mutable): Object, Array, Map, Set, Date, Function vs.
npx create-react-app counter-appile react projesi oluşturuldu.
npm install react-reduxile redux kuruldu. Redux ile react'ı bağlamak için gerekli
npm install @reduxjs/toolkitredux tool kit kuruldu. Redux kullanımı için gerekli
src/redux/store.js dosyası oluşturuldu ve içine:
import { configureStore } from "@reduxjs/toolkit";
export const store = configureStore({
reducer: {},
});
yazıldı. Bu kısım bizim datalarımızı tutan depomuz olacak.
Verilerin projedeki tüm kompanentlere ulaşması için src/index.js içine:
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { store } from "./redux/store"; // dataları yerleştireceğimiz kaynak
import { Provider } from "react-redux"; // react-redux bağlantısı
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}> {/* reduxtan gelen veri Providere prop verildi */}
<App />
</Provider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
redux/counter/counterSlice.js içinde:
import { createSlice } from "@reduxjs/toolkit";
export const counterSlice = createSlice({
name: "counter", // state e ulaşmak için kullanacağız.
initialState: { // statein başlangıç anındaki değerlerini girdiğimiz kısım
value: 0,
},
reducers: {
// statei günceleyecek tanımlar
},
});
export default counterSlice.reducer;
ile bir başlangıç değeri tanımladık.
Oluşturduğumuz veri deposunu src/redux/store.js içine import edip store yapısına dahil ettik.
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counter/counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer // counter keyine gelen veri yerleştirildi.
},
});
src/components/Counter.js komponentini oluşturup storedan çektiğimiz veriyi kullandık.
import React from 'react'
import { useSelector } from 'react-redux'
function Counter() {
const countValue = useSelector((state) => state.counter.value) // statein altındaki counter altındaki value değerini aldık.
console.log(countValue);
return (
<div>
<h1>{countValue}</h1>
</div>
)
}
export default Counter
Bu kompanenti de App.js içinde kullandık.
import './App.css';
import Counter from "./components/Counter";
function App() {
return (
<div className="App">
<Counter />
</div>
);
}
export default App;
redux/counter/counterSlice.js içindeki reducers alanına bu dosyada tutulan veriyi manipüle edecek fonksiyonları ekledik ve export ettik.
import { createSlice } from "@reduxjs/toolkit";
export const counterSlice = createSlice({
name: "counter", // state e ulaşmak için kullanacağız.
initialState: {
// statein başlangıç anındaki değerlerini girdiğimiz kısım
value: 0,
},
reducers: {
// statei günceleyecek tanımlar
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action)=> { // sayfadan alınan veriyi kullandığımız fonksiyonlar.
state.value += Number(action.payload); // action.payload fonksiyona parametre olarak girilen değeri verir. Bu veri obje dahil tüm veri tiplerinde olabilir. // veri string olarak geldi. Biz de Number fonksiyonu ile sayıya çevirdik.
},
decrementByAmount: (state, action)=> {
state.value -= Number(action.payload);
},
},
});
export const { increment, decrement, incrementByAmount, decrementByAmount } = counterSlice.actions; // fonksiyonları dışa aktarıyoruz.
export default counterSlice.reducer;
Veri manipülasyonu için yazdığımız fonksiyonları component/Counter.js içinde kullandık.
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, incrementByAmount, decrementByAmount } from "../redux/counter/counterSlice";
function Counter() {
const countValue = useSelector((state) => state.counter.value) // statein altındaki counter altındaki value değerini aldık.
const dispatch = useDispatch()
// useDispatch, dispatch değişkenine tanımlandı.
const [amount, setAmount] = useState(0) // formdan veri almak için oluşturuldu. formdan aldığı veriyi counterSlice içine gönderiyoruz.
return (
<div>
<h1>{countValue}</h1>
<button onClick={()=>dispatch(decrement())}>Decrement</button>
<button onClick={()=>dispatch(increment())}>Increment</button>
{/* dispatch içinde çağırdığımız fonksiyonu kullanıyoruz. */}
<br /><br />
<input type='number' value={amount} onChange={(e)=>setAmount(e.target.value)}/>
<br />
<button onClick={()=>dispatch(decrementByAmount(amount))}>Decrement by Amount</button>
<button onClick={()=>dispatch(incrementByAmount(amount))}>Increment by Amount</button>
</div>
)
}
export default Counter
Yeni react projesi oluşturulur.
Projenin görüntüsü için hocanın önceden hazırladığı kodlar kullanılır.
index.css hocanın css kodları ile değiştirildi.
src/components klasöründe kompanentlerimizi oluşturduk ve bir kısmını iç içe yazıp App.js içinde birleştirdik.
App.js:
import "./App.css";
import Content from "./components/Content";
import Footer from "./components/Footer";
import Header from "./components/Header";
function App() {
return (
<>
<section className="todoapp">
<Header />
<Content />
</section>
<Footer />
</>
);
}
export default App;
components/Header.js:
import React from "react";
import Form from "./Form";
function Header() {
return (
<header className="header">
<h1>todos</h1>
<Form />
</header>
);
}
export default Header;
components/Form.js:
import React from "react";
function Form() {
return (
<form>
<input
className="new-todo"
placeholder="What needs to be done?"
autoFocus
/>
</form>
);
}
export default Form;
components/Content.js:
import React from "react";
import TodoList from "./TodoList";
import ContentFooter from "./ContentFooter";
function Content() {
return (
<>
<section className="main">
<input className="toggle-all" type="checkbox" />
<label htmlFor="toggle-all">Mark all as complete</label>
<TodoList />
</section>
<ContentFooter />
</>
);
}
export default Content;
components/TodoList.js:
import React from "react";
function TodoList() {
return (
<ul className="todo-list">
<li className="completed">
<div className="view">
<input className="toggle" type="checkbox" />
<label>Learn JavaScript</label>
<button className="destroy"></button>
</div>
</li>
<li>
<div className="view">
<input className="toggle" type="checkbox" />
<label>Learn React</label>
<button className="destroy"></button>
</div>
</li>
<li>
<div className="view">
<input className="toggle" type="checkbox" />
<label>Have a life!</label>
<button className="destroy"></button>
</div>
</li>
</ul>
);
}
export default TodoList;
components/ContentFooter.js:
import React from "react";
function ContentFooter() {
return (
<footer className="footer">
<span className="todo-count">
<strong>2</strong>{" "}
items left
</span>
<ul className="filters">
<li>
<a href="#/" className="selected">
All
</a>
</li>
<li>
<a href="#/">Active</a>
</li>
<li>
<a href="#/">Completed</a>
</li>
</ul>
<button className="clear-completed">Clear completed</button>
</footer>
);
}
export default ContentFooter;
components/Footer.js:
import React from "react";
function Footer() {
return (
<footer className="info">
<p>Click to edit a todo</p>
<p>
Created by <a href="https://d12n.me/">Dmitry Sharabin</a>
</p>
<p>
Part of <a href="http://todomvc.com">TodoMVC</a>
</p>
</footer>
);
}
export default Footer;
react-redux ve redux-toolkit kuruldu
npm install @reduxjs/toolkit react-redux
redux/store.js içinde storeumuzu tanımladık.
import { configureStore } from "@reduxjs/toolkit";
import todosSlice from "./todos/todosSlice"; // todoSlice import edildi.
export const store = configureStore({
reducer: {
todos: todosSlice, // todoSlice içindeki veri storea yerleştirildi.
},
});
redux/todos/todosSlicer.js içinde todos verilerinin tutulduğu kısmı tanımladık ve içine yer tutucu veri girdik.
import { createSlice } from "@reduxjs/toolkit";
export const todosSlice = createSlice({
name: "todos",
initialState: {
items: [
{ // yer tutucu veriler.
id: "1",
title: "Learn React",
completed: true,
},
{
id: "2",
title: "Read a Book",
completed: false,
},
],
},
reducers: {},
});
export default todosSlice.reducer;
Verileri alabilmek için index.js içinde react-redux bağlantısını kurduk.
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import { store } from "./redux/store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
{/* react-redux bağlantısı */}
<App />
</Provider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
components/TodoList içinde veri çekildi ve kullanıldı.
import React from "react";
import { useSelector } from "react-redux"; // useSelector ile verilere ulaşırız.
function TodoList() {
const items = useSelector((state) => state.todos.items) // todos içindeki itemse ulaştık.
console.log("items", items);
return (
<ul className="todo-list">
{items.map((item)=>( // redux store üzerinden alınan veriyi kullandık.
<li key={item.id} className={item.completed ? "completed" : ""}>
<div className="view">
<input className="toggle" type="checkbox" />
<label>{item.title}</label>
<button className="destroy"></button>
</div>
</li>
))}
</ul>
);
}
export default TodoList;
redux/todos/todosSlice.js içinde reducers kısmına ekleme işlemi için fonksiyonumuzu yazdık ve export ettik
import { createSlice } from "@reduxjs/toolkit";
export const todosSlice = createSlice({
name: "todos",
initialState: {
items: [
{
id: "1",
title: "Learn React",
completed: true,
},
{
id: "2",
title: "Read a Book",
completed: false,
},
],
},
reducers: {
addTodo: (state, action) => {
state.items.push(action.payload);
},
},
});
export const { addTodo } = todosSlice.actions; // fonksiyon export edildi.
export default todosSlice.reducer;
Gelen fonksiyonu components/Form.js içinde import ve dispatch edip kullandık.
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addTodo } from "../redux/todos/todosSlice";
import { nanoid } from "@reduxjs/toolkit"; // unique id yaratmak için kullanılır.
function Form() {
const [title, setTitle] = useState("") // formdan gelen veriyi tutar
const dispatch = useDispatch()
const handleSubmit = (e) => {
e.preventDefault(); // formun default davranışını sıfırlar
dispatch(addTodo({ id: nanoid(), title, completed: false}))
setTitle("") // form submit edildikten sonra formu temizler.
};
return (
<form onSubmit={handleSubmit}>
<input
className="new-todo"
placeholder="What needs to be done?"
autoFocus
value={title}
onChange={(e)=>setTitle(e.target.value)}
/>
</form>
);
}
export default Form;
Bu kısımda listelenen göreve tıkladığımızda üzerini çizilmesi işlemini yapacağız.
redux/todos/todosSlice.js içindeki reducers kısmına yeni bir fonksiyon ekledik ve export ettik.
import { createSlice } from "@reduxjs/toolkit";
export const todosSlice = createSlice({
name: "todos",
initialState: {
items: [
{
id: "1",
title: "Learn React",
completed: true,
},
{
id: "2",
title: "Read a Book",
completed: false,
},
],
},
reducers: {
addTodo: (state, action) => {
state.items.push(action.payload);
},
toggle: (state, action) => {
const { id } = action.payload; // tıklanan kompanentin id sini alır.
const item = state.items.find((item) => item.id === id); // id ye göre itemi bulur.
item.completed = !item.completed; // completed verisini true ise false false ise true yapar.
},
},
});
export const { addTodo, toggle } = todosSlice.actions; // fonksiyon export edildi.
export default todosSlice.reducer;
components/TodaList.js içinde bu fonksiyonu kullandık.
import React from "react";
import { useSelector, useDispatch } from "react-redux"; // useDispatch ile fonksiyonlara ulaşırız.
import { toggle } from "../redux/todos/todosSlice";
function TodoList() {
const dispatch = useDispatch();
const items = useSelector((state) => state.todos.items);
return (
<ul className="todo-list">
{items.map((item) => (
<li key={item.id} className={item.completed ? "completed" : ""}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={item.completed} // completed durumuna göre tiki aktif eder.
onChange={() => dispatch(toggle({ id: item.id }))} // tıklanınca fonksiyonu çalıştırır.
/>
<label>{item.title}</label>
<button className="destroy"></button>
</div>
</li>
))}
</ul>
);
}
export default TodoList;
Silme fonksiyonu todoSlice.js reducers alanında tanımlanır ve export edilir.
import { createSlice } from "@reduxjs/toolkit";
export const todosSlice = createSlice({
name: "todos",
initialState: {
items: [
{
id: "1",
title: "Learn React",
completed: true,
},
{
id: "2",
title: "Read a Book",
completed: false,
},
],
},
reducers: {
addTodo: (state, action) => {
state.items.push(action.payload);
},
toggle: (state, action) => {
const { id } = action.payload;
const item = state.items.find((item) => item.id === id);
item.completed = !item.completed;
},
destroy: (state, action) => {
const id = action.payload; // id alındı.
const filtered = state.items.filter((item) => item.id !== id); // id si gelen parametre olmayan tüm elemanlar alındı
state.items = filtered; // filtrelenen bu elemanlar yeni state.items olarak tanıtıldı.
},
},
});
export const { addTodo, toggle, destroy } = todosSlice.actions; // fonksiyon export edildi.
export default todosSlice.reducer;
Silme fonksiyonu TodoList.js içinde import edilir ve kullanılır.
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { toggle, destroy } from "../redux/todos/todosSlice"; // fonksiyon import edildi
function TodoList() {
const dispatch = useDispatch();
const items = useSelector((state) => state.todos.items);
const handleDestroy = (id) => {
if (window.confirm("Are You Sure?")) { // confirm işlemi
dispatch(destroy(id)); // silme işlemi
}
};
return (
<ul className="todo-list">
{items.map((item) => (
<li key={item.id} className={item.completed ? "completed" : ""}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={item.completed}
onChange={() => dispatch(toggle({ id: item.id }))}
/>
<label>{item.title}</label>
<button
className="destroy"
onClick={() => handleDestroy(item.id)} // silme fonksyonunu başlatan buton
></button>
</div>
</li>
))}
</ul>
);
}
export default TodoList;
Filte bilgisi için todosSlice.js initialState alanına yeni bir değişken eklendi. Aktif filtre bilgisini değiştirmek için reducers alanına bir fonksiyon eklendi. Aktif olmayan tüm verileri silmek için de bir fonksiyon eklendi.
import { createSlice } from "@reduxjs/toolkit";
export const todosSlice = createSlice({
name: "todos",
initialState: {
items: [
{
id: "1",
title: "Learn React",
completed: true,
},
{
id: "2",
title: "Read a Book",
completed: false,
},
],
activeFilter: "all", // aktif filtre verisi tutulur.
},
reducers: {
addTodo: (state, action) => {
state.items.push(action.payload);
},
toggle: (state, action) => {
const { id } = action.payload;
const item = state.items.find((item) => item.id === id);
item.completed = !item.completed;
},
destroy: (state, action) => {
const id = action.payload;
const filtered = state.items.filter((item) => item.id !== id);
state.items = filtered;
},
changeActiveFilter: (state, action) => {
// aktif filtreyi değiştiren fonksiyon
state.activeFilter = action.payload;
},
clearCompleted: (state) => {
// aktif olmayanları silme fonksiyonu
const filtered = state.items.filter((item) => item.completed === false);
state.items = filtered;
},
},
});
export const { addTodo, toggle, destroy, changeActiveFilter, clearCompleted } =
todosSlice.actions; // fonksiyon export edildi.
export default todosSlice.reducer;
ContentFooter.js içinde aktif filtre verisi, bu veriyi değiştiren fonksiyon ve aktif olmayan nesneleri silen fonksiyon import edilip kullanıldı.
import React from "react";
import { useSelector, useDispatch } from "react-redux"; // useSelector ve useDispatch import edildi.
import { changeActiveFilter, clearCompleted } from "../redux/todos/todosSlice"; // fonksiyon import edildi.
function ContentFooter() {
const dispatch = useDispatch();
const items = useSelector((state) => state.todos.items); // redux store.js üzerinden veriler çekildi.
const itemsLeft = items.filter((item) => !item.completed).length; // tamamlanmamış olanların sayısını verir.
const activeFilter = useSelector((state) => state.todos.activeFilter); // aktif filtre verisi alındı.
console.log(itemsLeft);
return (
<footer className="footer">
<span className="todo-count">
<strong>{itemsLeft}</strong> item{itemsLeft > 1 && "s"} left
</span>
<ul className="filters">
<li>
<a
href="#/"
className={activeFilter === "all" ? "selected" : ""} // selected clası için şart konuldu
onClick={() => dispatch(changeActiveFilter("all"))} // aktif filtre bilgisini değiştiren fonksiyon
>
{" "}
All
</a>
</li>
<li>
<a
href="#/"
className={activeFilter === "active" ? "selected" : ""} // selected clası için şart konuldu
onClick={() => dispatch(changeActiveFilter("active"))} // aktif filtre bilgisini değiştiren fonksiyon
>
Active
</a>{" "}
</li>
<li>
<a
href="#/"
className={activeFilter === "completed" ? "selected" : ""} // selected clası için şart konuldu
onClick={() => dispatch(changeActiveFilter("completed"))} // aktif filtre bilgisini değiştiren fonksiyon
>
Completed
</a>{" "}
</li>
</ul>
<button
className="clear-completed"
onClick={() => dispatch(clearCompleted())} // aktif olmayanları silme fonksiyonu
>
Clear completed
</button>
</footer>
);
}
export default ContentFooter;
Aktif filte bilgisi TodoList.js içinde import edildi ve filteleme durumuna göre gösterilecek verileri ayarlamak için kullanıldı.
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { toggle, destroy } from "../redux/todos/todosSlice";
let filtered = []; // filtreleme işlemi için kullanılacak
function TodoList() {
const dispatch = useDispatch();
const items = useSelector((state) => state.todos.items);
const activeFilter = useSelector((state) => state.todos.activeFilter); // aktif olan filtre bilgisi alındı.
const handleDestroy = (id) => {
if (window.confirm("Are You Sure?")) {
dispatch(destroy(id));
}
};
filtered = items; //activeFilter all ise tüm veriyi göster.
if (activeFilter !== "all") {
// activeFilter all değil ise koşulu başlat.
filtered = items.filter((todo) =>
activeFilter === "active" // activeFilter active ise tamamlanmamamışları değil ise tamamlanmışları göster.
? todo.completed === false
: todo.completed === true
);
}
return (
<ul className="todo-list">
{filtered.map((item) => (
<li key={item.id} className={item.completed ? "completed" : ""}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={item.completed}
onChange={() => dispatch(toggle({ id: item.id }))}
/>
<label>{item.title}</label>
<button
className="destroy"
onClick={() => handleDestroy(item.id)}
></button>
</div>
</li>
))}
</ul>
);
}
export default TodoList;
Selector birden fazla yerde kullanılacak verileri hazırlayıp kullanmak için hazırlanır. Tek sefer kullanılacak veriler için gerekli değildir.
useSelector() ifadesinde parantez içinde kullandığımız ifadeyi todosSlice.js içinde selector olarak tanımlayıp export edebiliriz.
Daha önceden yaptığımız filtreleme işlemini de selector olarak yazabiliriz.
import { createSlice } from "@reduxjs/toolkit";
export const todosSlice = createSlice({
name: "todos",
initialState: {
items: [
{
id: "1",
title: "Learn React",
completed: true,
},
{
id: "2",
title: "Read a Book",
completed: false,
},
],
activeFilter: "all",
},
reducers: {
addTodo: (state, action) => {
state.items.push(action.payload);
},
toggle: (state, action) => {
const { id } = action.payload;
const item = state.items.find((item) => item.id === id);
item.completed = !item.completed;
},
destroy: (state, action) => {
const id = action.payload;
const filtered = state.items.filter((item) => item.id !== id);
state.items = filtered;
},
changeActiveFilter: (state, action) => {
state.activeFilter = action.payload;
},
clearCompleted: (state) => {
const filtered = state.items.filter((item) => item.completed === false);
state.items = filtered;
},
},
});
export const selectTodos = (state) => state.todos.items; // import edildiği yerde useSelect() parantezi içinde kullanıldığında items verisini verir.
export const selectFilteredTodos = (state) => {
if (state.todos.activeFilter === "all") {
return state.todos.items;
}
return state.todos.items.filter((todo) =>
state.todos.activeFilter === "active"
? todo.completed === false
: todo.completed === true
);
};
export const { addTodo, toggle, destroy, changeActiveFilter, clearCompleted } =
todosSlice.actions;
export default todosSlice.reducer;
export ettiğimiz bu iki selector TodoList.js içinde kullanır.
TodoList.js
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import {
toggle,
destroy,
selectFilteredTodos,
} from "../redux/todos/todosSlice";
function TodoList() {
const dispatch = useDispatch();
const filteredTodos = useSelector(selectFilteredTodos); // filtreleme işlemini kaynağında yaptığımız veriyi değişkene atadık ve aşağıda mapledik.
const handleDestroy = (id) => {
if (window.confirm("Are You Sure?")) {
dispatch(destroy(id));
}
};
return (
<ul className="todo-list">
{filteredTodos.map((item) => (
<li key={item.id} className={item.completed ? "completed" : ""}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={item.completed}
onChange={() => dispatch(toggle({ id: item.id }))}
/>
<label>{item.title}</label>
<button
className="destroy"
onClick={() => handleDestroy(item.id)}
></button>
</div>
</li>
))}
</ul>
);
}
export default TodoList;
Footer.js stabil ögelerden oluştuğu için her seferinde render edilmesine gerek yok. Bu nedenle React.memo() ile sarmalandı.
import React from "react";
function Footer() {
return (
<footer className="info">
<p>Click to edit a todo</p>
<p>
Created by <a href="https://d12n.me/">Dmitry Sharabin</a>
</p>
<p>
Part of <a href="http://todomvc.com">TodoMVC</a>
</p>
</footer>
);
}
export default React.memo(Footer); // herhangi bir değişken içermediğinden her seferinde render edilmesi engellendi.
Form.js içinde formun boşken çalışmaması için koşul eklendi.
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addTodo } from "../redux/todos/todosSlice";
import { nanoid } from "@reduxjs/toolkit";
function Form() {
const [title, setTitle] = useState("");
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (!title) return; // title boşsa hiçbir işlem yapmadan return eder.
dispatch(addTodo({ id: nanoid(), title, completed: false }));
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
className="new-todo"
placeholder="What needs to be done?"
autoFocus
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</form>
);
}
export default Form;
reducers statei değiştirmeden önce ona gelecek payloadı yapılandırmak için kullanılır.
Form.js içinde addTodo fonksiyonu kullanırken title ile beraber gönderdiğimiz completed ve id bilgilerini prepare olarak todoSlice.js içinde fonksiyonun kendisine taşıdık.
import { createSlice, nanoid } from "@reduxjs/toolkit";
export const todosSlice = createSlice({
name: "todos",
initialState: {
items: [
{
id: "1",
title: "Learn React",
completed: true,
},
{
id: "2",
title: "Read a Book",
completed: false,
},
],
activeFilter: "all",
},
reducers: {
addTodo: {
reducer: (state, action) => {
// çalışacak olan fonksiyon
state.items.push(action.payload);
},
prepare: ({ title }) => { // reducer öncesi payloada yapılan düzenleme.
return {
payload: { // burada return edilen payload action içine düşüyor.
id: nanoid(), // modül import edildi.
completed: false,
title,
},
};
},
},
...
...
},
});
...
...
export const { addTodo, toggle, destroy, changeActiveFilter, clearCompleted } =
todosSlice.actions;
export default todosSlice.reducer;
Form.js içinde addTodo fonksiyonu sadece {title} payload ile gönderilecek şekilde düzenlendi.
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addTodo } from "../redux/todos/todosSlice";
function Form() {
const [title, setTitle] = useState("");
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (!title) return;
dispatch(addTodo({ title })); // modifiye edilen kod.
setTitle("");
};
return (
...
);
}
export default Form;
Hoca hazır beckand verdi. Kendi uygulamamızı client klasörüne taşıdık. Hocanın backendini api klasörüne yükledik. terminale:
cd api
npm i
yazarak api için gereken modülleri yükledik
npm run serverile backendi çalıştırdık.
Backend localhost:7000 üzerinden çalışıyor. Bir veri tabanına bağlı değil. İçinde veri tabanını simule edecek bir array mevcut. Backend çalıştığı sürece bu arrayi manipule edeceğiz. Backend resetlenince veri de resetlenecek. İşlevsel fonksiyonların tamamı server.js içinde.
const express = require('express');
const dotenv = require('dotenv');
const colors = require('colors');
const cors = require('cors');
const { json } = require('body-parser');
const { nanoid } = require('nanoid');
dotenv.config({ path: './config.env' });
const app = express();
app.use(cors());
app.use(json());
let todos = [
{
id: nanoid(),
title: 'todo 1',
completed: true,
},
{
id: nanoid(),
title: 'todo 2',
completed: false,
},
{
id: nanoid(),
title: 'todo 3',
completed: false,
},
{
id: nanoid(),
title: 'todo 4',
completed: false,
},
{
id: nanoid(),
title: 'todo 5',
completed: false,
},
];
app.get('/todos', (req, res) => res.send(todos));
app.post('/todos', (req, res) => {
const todo = { title: req.body.title, id: nanoid(), completed: false };
todos.push(todo);
return res.send(todo);
});
app.patch('/todos/:id', (req, res) => {
const id = req.params.id;
const index = todos.findIndex((todo) => todo.id == id);
const completed = Boolean(req.body.completed);
if (index > -1) {
todos[index].completed = completed;
}
return res.send(todos[index]);
});
app.delete('/todos/:id', (req, res) => {
const id = req.params.id;
const index = todos.findIndex((todo) => todo.id == id);
if (index > -1) {
todos.splice(index, 1);
}
res.send(todos);
});
const PORT = 7000;
app.listen(PORT, console.log(`Server running on port ${PORT}`.green.bold));
Asenkron işlemler için middleware gerekebiliyor. fetch, login vs
todosSlice.js içinde Thunk Middleware import edildi ve kullanıldı.
import { createSlice, nanoid, createAsyncThunk } from "@reduxjs/toolkit"; //Thunk middlewarei import edildi.
import axios from "axios";
export const getTodosAsync = createAsyncThunk(
"todos/getTodosAysnc/",
/* async () => {
const res = await fetch("http://localhost:7000/todos");
return await res.json();
} */
// aynı işlem yukarıda fetch aşağıda axios kullanılarak yapıldı.
async () => {
const res = await axios("http://localhost:7000/todos");
return res.data;
}
); // api ile haberleşen middleware. ilk parametrede action name ikinci parametrede fetch işlemi yazılır. extraReducers alanında da hangi durumunda ne yapılacağı yazılır.
export const todosSlice = createSlice({
name: "todos",
initialState: {
items: [],
isLoading: false,
error: null,
activeFilter: "all",
},
reducers: {
addTodo: {
reducer: (state, action) => {
state.items.push(action.payload);
},
prepare: ({ title }) => {
return {
payload: {
id: nanoid(),
completed: false,
title,
},
};
},
},
toggle: (state, action) => {
const { id } = action.payload;
const item = state.items.find((item) => item.id === id);
item.completed = !item.completed;
},
destroy: (state, action) => {
const id = action.payload;
const filtered = state.items.filter((item) => item.id !== id);
state.items = filtered;
},
changeActiveFilter: (state, action) => {
state.activeFilter = action.payload;
},
clearCompleted: (state) => {
const filtered = state.items.filter((item) => item.completed === false);
state.items = filtered;
},
},
extraReducers: {
//action name tanımı thunk ile verilen işlemin pending, fulfilled ve rejected durumlarına işlevler tanımlandı.
[getTodosAsync.pending]: (state, action) => {
// getTodosAsync pending (bekleme) durumunda ise aşağıdaki kısım çalışır.
state.isLoading = true;
},
[getTodosAsync.fulfilled]: (state, action) => {
// getTodosAsync fulfilled (başarılı) ise aşağıdaki fonksiyon çalışır.
state.items = action.payload;
state.isLoading = false;
},
[getTodosAsync.rejected]: (state, action) => {
// getTodos rejected (reddedilmiş) ise aşağıdaki fonksiyon çalışır.
state.isLoading = false;
state.error = action.error.message;
},
},
});
export const selectTodos = (state) => state.todos.items;
export const selectFilteredTodos = (state) => {
if (state.todos.activeFilter === "all") {
return state.todos.items;
}
return state.todos.items.filter((todo) =>
state.todos.activeFilter === "active"
? todo.completed === false
: todo.completed === true
);
};
export const { addTodo, toggle, destroy, changeActiveFilter, clearCompleted } =
todosSlice.actions;
export default todosSlice.reducer;
Fetch işleminde axios kullanabilmek için terminale:
npm i axios
todosSlice.js içinde tanımlanan getTodosAsync
fonksiyonu TodoList.js kompanentinde kullanıldı.
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import {
toggle,
destroy,
selectFilteredTodos,
getTodosAsync,
} from "../redux/todos/todosSlice";
import Loading from "./Loading";
import Error from "./Error";
function TodoList() {
const dispatch = useDispatch();
const filteredTodos = useSelector(selectFilteredTodos);
const isLoading = useSelector((state) => state.todos.isLoading); // isLoading verisi alındı.
const error = useSelector((state) => state.todos.error); // hata mesajı alındı
useEffect(() => {
// sayfa mount edildiğinde backendden data çekmek için yazdığımız fonksiyonu çalıştırır.
dispatch(getTodosAsync());
}, [dispatch]);
const handleDestroy = (id) => {
if (window.confirm("Are You Sure?")) {
dispatch(destroy(id));
}
};
if (isLoading) {
// isLoading koşulu.
return <Loading />;
}
if (error) {
// error koşulu
return <Error message={error}/>;
}
return (
<ul className="todo-list">
{filteredTodos.map((item) => (
<li key={item.id} className={item.completed ? "completed" : ""}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={item.completed}
onChange={() => dispatch(toggle({ id: item.id }))}
/>
<label>{item.title}</label>
<button
className="destroy"
onClick={() => handleDestroy(item.id)}
></button>
</div>
</li>
))}
</ul>
);
}
export default TodoList;
Components klasörü içerisinde Loading.js ve Error.js TodoList.js içinde kullanılmak üzere oluşturuldu.
import React from 'react'
function Loading() {
return (
<div style={{ padding: 15, fontSize: 18}}>Loading....</div>
)
}
export default Loading
import React from "react";
function Error({ message }) {
return <div style={{padding: 15, fontSize: 16}}>Error: {message}</div>;
}
export default Error;
fetch işlemlerini sadeleştirmek için backend endpoint .env
dosyasına tanımlanır.
REACT_APP_API_BASE_ENDPOINT=http://localhost:7000
todosSlice.js dosyasında post işlemi tanımlanır.
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const getTodosAsync = createAsyncThunk(
"todos/getTodosAysnc/",
async () => {
const res = await axios(`${process.env.REACT_APP_API_BASE_ENDPOINT}/todos`);
return res.data;
}
);
export const addTodosAsync = createAsyncThunk(
"todos/addTodoAsync",
async (data) => {
const res = await axios.post(`${process.env.REACT_APP_API_BASE_ENDPOINT}/todos`, data);
return res.data;
}
); // data olarak gelen veriyi backende post eden fonksiyon.
export const todosSlice = createSlice({
name: "todos",
initialState: {
items: [],
isLoading: false,
error: null,
activeFilter: "all",
addNewTodoIsLoading: false,
addNewTodoError: null,
},
reducers: {
toggle: (state, action) => {
const { id } = action.payload;
const item = state.items.find((item) => item.id === id);
item.completed = !item.completed;
},
destroy: (state, action) => {
const id = action.payload;
const filtered = state.items.filter((item) => item.id !== id);
state.items = filtered;
},
changeActiveFilter: (state, action) => {
state.activeFilter = action.payload;
},
clearCompleted: (state) => {
const filtered = state.items.filter((item) => item.completed === false);
state.items = filtered;
},
},
extraReducers: {
// get todos
[getTodosAsync.pending]: (state, action) => {
state.isLoading = true;
},
[getTodosAsync.fulfilled]: (state, action) => {
state.items = action.payload;
state.isLoading = false;
},
[getTodosAsync.rejected]: (state, action) => {
state.isLoading = false;
state.error = action.error.message;
},
// add todo
[addTodosAsync.pending]: (state, action) => {
state.addNewTodoIsLoading = true;
},
[addTodosAsync.fulfilled]: (state, action) => {
state.items.push(action.payload);
state.addNewTodoIsLoading = false;
},
[addTodosAsync.rejected]: (state, action) => {
state.addNewTodoIsLoading = false;
state.addNewTodoError = action.error.message;
},
},
});
export const selectTodos = (state) => state.todos.items;
export const selectFilteredTodos = (state) => {
if (state.todos.activeFilter === "all") {
return state.todos.items;
}
return state.todos.items.filter((todo) =>
state.todos.activeFilter === "active"
? todo.completed === false
: todo.completed === true
);
};
export const { toggle, destroy, changeActiveFilter, clearCompleted } =
todosSlice.actions;
export default todosSlice.reducer;
Post işleminin fonksiyonu Form.js içinde import edilip kullanıldı. Loading ve error durumları için düzenleme yapıldı.
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { addTodosAsync } from "../redux/todos/todosSlice"; // yeni todo eklemek için kullanılacak fonksiyon
import Loading from "./Loading";
import Error from "./Error"
function Form() {
const [title, setTitle] = useState("");
const dispatch = useDispatch();
const isLoading = useSelector((state) => state.todos.addNewTodoIsLoading); // Loading için gereken veri
const error = useSelector((state) => state.todos.addNewTodoError); // error mesajı
console.log("error:", error);
const handleSubmit = async (e) => {
// fonksiyon asenkron çalışmalı.
e.preventDefault();
if (!title) return;
await dispatch(addTodosAsync({ title })) // todo ekleme fonksiyonu
setTitle("");
};
return (
<form
onSubmit={handleSubmit}
style={{ display: "flex", alignItems: "center" }}
>
{/* Loading için stil tanımı */}
<input
disabled={isLoading} // isLoading true ise formu disable eder.
className="new-todo"
placeholder="What needs to be done?"
autoFocus
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
{isLoading && <Loading />}
{/* Loading true ise görünür. Yoksa görünmez. */}
{error && <Error message={error}/>}
</form>
);
}
export default Form;
Yapılan işlemler arttıkça kodun okunaklı kalması için middleware ile yaptığımız fetch işlemlerini todos/services.js dosyasına taşıdık ve toggle ve delete işlemi tanımlandı.
import { createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const getTodosAsync = createAsyncThunk(
"todos/getTodosAysnc/",
async () => {
const res = await axios(`${process.env.REACT_APP_API_BASE_ENDPOINT}/todos`);
return res.data;
}
);
export const addTodosAsync = createAsyncThunk(
"todos/addTodoAsync",
async (data) => {
const res = await axios.post(
`${process.env.REACT_APP_API_BASE_ENDPOINT}/todos`,
data
);
return res.data;
}
);
export const toggleTodoAsync = createAsyncThunk(
"todos/toggleTodoAsync",
async ({ id, data }) => {
const res = await axios.patch(
`${process.env.REACT_APP_API_BASE_ENDPOINT}/todos/${id}`,
data
);
return res.data;
}
); // backend tarafında toggle işlemi yapacak fonksiyon. data ile completed verisi alınacak.
export const removeTodoAsync = createAsyncThunk(
"todos/removeTodoAsync",
async (id) => {
await axios.delete(
`${process.env.REACT_APP_API_BASE_ENDPOINT}/todos/${id}`
);
return id; // buradan gelen data extrareducers alanındaki action.payload olarak dönüyor.
}
); // delete işlemi
services.js içindeki fonksiyonlar todosSlice.js içine import edildi. extraReducers alanında düzenlemeler yapıldı.
import { createSlice } from "@reduxjs/toolkit";
import {addTodosAsync, getTodosAsync, toggleTodoAsync, removeTodoAsync } from "./services" // middleware ile fetch işlemlerini services.js klasörüne taşıdık ve oradan import ettik.
export const todosSlice = createSlice({
name: "todos",
initialState: {
items: [],
isLoading: false,
error: null,
activeFilter: localStorage.getItem("activeFilter") || "all",
addNewTodo: {
// bu yazım öncekinden daha okunaklı. Buna göre extraReducers ve Form.js alanlarını düzenledik.
isLoading: false,
error: null,
},
},
reducers: {
changeActiveFilter: (state, action) => {
state.activeFilter = action.payload;
},
clearCompleted: (state) => {
const filtered = state.items.filter((item) => item.completed === false);
state.items = filtered;
},
},
extraReducers: {
// backend tarafına işlem yapılırken client tarafında eşgüdüm sağlar.
// get todos
[getTodosAsync.pending]: (state, action) => {
state.isLoading = true;
},
[getTodosAsync.fulfilled]: (state, action) => {
state.items = action.payload;
state.isLoading = false;
},
[getTodosAsync.rejected]: (state, action) => {
state.isLoading = false;
state.error = action.error.message;
},
// add todo
[addTodosAsync.pending]: (state, action) => {
state.addNewTodo.isLoading = true;
},
[addTodosAsync.fulfilled]: (state, action) => {
state.items.push(action.payload);
state.addNewTodo.isLoading = false;
},
[addTodosAsync.rejected]: (state, action) => {
state.addNewTodo.isLoading = false;
state.addNewTodo.error = action.error.message;
},
// toggle todo
[toggleTodoAsync.fulfilled]: (state, action) => {
const { id, completed } = action.payload;
const index = state.items.findIndex((item) => item.id === id);
state.items[index].completed = completed;
},
// remove todo
[removeTodoAsync.fulfilled]: (state, action) => {
// 1. yöntem
// const id = action.payload;
// const filtered = state.items.filter((item) => item.id !== id);
// state.items = filtered;
// 2. yöntem
const id = action.payload;
const index = state.items.findIndex((item) => item.id === id);
state.items.splice(index, 1);
},
},
});
export const selectTodos = (state) => state.todos.items;
export const selectFilteredTodos = (state) => {
if (state.todos.activeFilter === "all") {
return state.todos.items;
}
return state.todos.items.filter((todo) =>
state.todos.activeFilter === "active"
? todo.completed === false
: todo.completed === true
);
};
export const { changeActiveFilter, clearCompleted } = todosSlice.actions;
export default todosSlice.reducer;
Daha önce fonksiyonları todoSlice.js içinden import ediyorduk. Artık services.js içinden import ediyoruz. Buna göre diğer sayfalardaki import işlemleri yenilenir.
TodoList.js içinde toggle ve delete için yazılan fonksiyonlar kullanıldı.
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { selectFilteredTodos } from "../redux/todos/todosSlice";
import {
getTodosAsync,
toggleTodoAsync,
removeTodoAsync,
} from "../redux/todos/services";
import Loading from "./Loading";
import Error from "./Error";
function TodoList() {
const dispatch = useDispatch();
const filteredTodos = useSelector(selectFilteredTodos);
const isLoading = useSelector((state) => state.todos.isLoading);
const error = useSelector((state) => state.todos.error);
useEffect(() => {
dispatch(getTodosAsync());
}, [dispatch]);
const handleDestroy = async (id) => {
if (window.confirm("Are You Sure?")) {
await dispatch(removeTodoAsync(id)); // silme fonksiyonu
}
};
const handleToggle = async (id, completed) => {
await dispatch(toggleTodoAsync({ id, data: { completed } }));
}; // toggle fonksiyonu
if (isLoading) {
return <Loading />;
}
if (error) {
return <Error message={error} />;
}
return (
<ul className="todo-list">
{filteredTodos.map((item) => (
<li key={item.id} className={item.completed ? "completed" : ""}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={item.completed}
onChange={() => handleToggle(item.id, !item.completed)} // toggle için fonksiyonu kullanırken completed verisinin mevcut halini değil, tıklanınca istediğimiz halini gönderiyoruz.
/>
<label>{item.title}</label>
<button
className="destroy"
onClick={() => handleDestroy(item.id)} // silme fonksiyonu
></button>
</div>
</li>
))}
</ul>
);
}
export default TodoList;
ContentFooter kompanentinde kullanılan activeFilter verisi localStorage üzerine eklendi. Buradan alınan veri de todosSlice.js initialState alanındaki default activeFilter alanında kullanıldı. Bu sayede sayfa yenilense de filtre bilgisi korunmuş oldu.
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import {
changeActiveFilter,
clearCompleted,
selectTodos,
} from "../redux/todos/todosSlice";
function ContentFooter() {
const dispatch = useDispatch();
const items = useSelector(selectTodos);
const itemsLeft = items.filter((item) => !item.completed).length;
const activeFilter = useSelector((state) => state.todos.activeFilter);
console.log(itemsLeft);
useEffect(() => {
localStorage.setItem("activeFilter", activeFilter)
},[activeFilter])
return (
<footer className="footer">
<span className="todo-count">
<strong>{itemsLeft}</strong> item{itemsLeft > 1 && "s"} left
</span>
<ul className="filters">
<li>
<a
href="#/"
className={activeFilter === "all" ? "selected" : ""}
onClick={() => dispatch(changeActiveFilter("all"))}
>
{" "}
All
</a>
</li>
<li>
<a
href="#/"
className={activeFilter === "active" ? "selected" : ""}
onClick={() => dispatch(changeActiveFilter("active"))}
>
Active
</a>{" "}
</li>
<li>
<a
href="#/"
className={activeFilter === "completed" ? "selected" : ""}
onClick={() => dispatch(changeActiveFilter("completed"))}
>
Completed
</a>{" "}
</li>
</ul>
<button
className="clear-completed"
onClick={() => dispatch(clearCompleted())}
>
Clear completed
</button>
</footer>
);
}
export default ContentFooter;
Backend tarafında completed: true ögeleri silen bir fonksiyon yazıldı.
app.put("/todos", async (req, res) => {
console.log("requset ulaştı");
const uncompleted = todos.filter((item) => item.completed === false);
todos = uncompleted;
res.send(todos);
}); // put request olarak completed false olan ögeleri filtreledik. kalanları response olarak gönderdik.
Bu fonksiyona client services.js içinden istek gönderildi.
export const deleteCompletedTodos = createAsyncThunk("todos/deleteCompletedTodos", async() => {
const res = await axios.put(
`${process.env.REACT_APP_API_BASE_ENDPOINT}/todos`
)
return res.data
}) // silme işlemi için tanımlanan fonksiyon.
İlgili fonksiyonun client işlemleri için todoSlice.js extraReducers alanında ilgili tanımlamalar yapıldı.
...
extraReducers: {
...
// delete completed todos
[deleteCompletedTodos.fulfilled]: (state, action) => {
console.log(action.payload);
state.items = action.payload
},
[deleteCompletedTodos.rejected]: (state, action) => {
console.log(action.error.message);
}
},
...
Fonksiyon ContentFooter.js içinde import edildi ve kullanıldı.
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import {
changeActiveFilter,
selectTodos,
} from "../redux/todos/todosSlice";
import { deleteCompletedTodos } from "../redux/todos/services"; // tamamlananları silme fonksiyonu
function ContentFooter() {
const dispatch = useDispatch();
const items = useSelector(selectTodos);
const itemsLeft = items.filter((item) => !item.completed).length;
const activeFilter = useSelector((state) => state.todos.activeFilter);
console.log(itemsLeft);
useEffect(() => {
localStorage.setItem("activeFilter", activeFilter)
},[activeFilter])
return (
<footer className="footer">
<span className="todo-count">
<strong>{itemsLeft}</strong> item{itemsLeft > 1 && "s"} left
</span>
<ul className="filters">
<li>
<a
href="#/"
className={activeFilter === "all" ? "selected" : ""}
onClick={() => dispatch(changeActiveFilter("all"))}
>
{" "}
All
</a>
</li>
<li>
<a
href="#/"
className={activeFilter === "active" ? "selected" : ""}
onClick={() => dispatch(changeActiveFilter("active"))}
>
Active
</a>{" "}
</li>
<li>
<a
href="#/"
className={activeFilter === "completed" ? "selected" : ""}
onClick={() => dispatch(changeActiveFilter("completed"))}
>
Completed
</a>{" "}
</li>
</ul>
<button
className="clear-completed"
onClick={() => dispatch(deleteCompletedTodos())} // tamamlananları silme butonu
>
Clear completed
</button>
</footer>
);
}
export default ContentFooter;
bbapi klasörü oluşturup vsc ile açtık ve terminale
npx create-react-app .yazdık.
proje oluşturulduulduktan sonra terminale
npm i react-router-domyazarak react-router-dom kurulur.
Hova react-router-dom v5 kullanıyor. Biz v6 ile yapacağız. Başlangıç için kullanılacak örnek aşağıdadır.
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, don't you think?</p>
</main>
<nav>
<Link to="/">Home</Link>
</nav>
</>
);
}
export default App;
redux toolkit ve react-redux kurulumu için terminale:
npm install react-redux @reduxjs/toolkit
redux store kurulumu için redux/store.js dosyası aşağıdaki gibi düzenlenir.
import { configureStore } from "@reduxjs/toolkit";
export const store = configureStore({
reducer: {}
})
index.js içinde App
kompanenti react-redux bileşeni olan Provider
ile sarmalanır.
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import { store } from "./redux/store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Hocanın örnek için kullandığı Breaking Bad Api kullanımdan kalkmış. Onun yerine harry potter api kullanılacak
Hocanın kullandığı api kendi offset ve limit sorgusuna sahip. Biz bunu farklı yöneteceğiz.
kök dizine .env dosyası oluşturulup base endpoint ortam değişleni olarak tanımlandı.
REACT_APP_API_BASE_ENDPOINT=https://hp-api.onrender.com/api
fatch işlemi için axios modülü kullandık. Bunun için terminale:
npm i axios
redux/charactersSlice.js dosyası oluşturuldu ve içinde createAsyncThunk metodu ile fetch işlemi yapıldı.
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
export const fetchCharacters = createAsyncThunk("characters/getCharacters", async ()=>{
const res = await axios(`${process.env.REACT_APP_API_BASE_ENDPOINT}/characters`)
return res.data
})
export const charactersSlice = createSlice({
name: "character",
initialState: {
items: [],
},
reducers: {},
extraReducers: {
[fetchCharacters.fulfilled]: (state, action) => {
console.log(action.payload);
}
},
});
export default charactersSlice.reducer;
redux/store.js içinde charactersSlice import edildi ve kullanıldı.
import { configureStore } from "@reduxjs/toolkit";
import charactersSlice from "./charactersSlice"
export const store = configureStore({
reducer: {
characters: charactersSlice
}
})
pages/Home/index.js içinde charactersSlice içindeki fonksiyon imort edildi. dispatch edildi ve useEffect içinde kullanıldı.
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchCharacters } from "../../redux/charactersSlice";
function Home() {
const data = useSelector((state) => state.characters);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchCharacters());
}, [dispatch]);
console.log(data);
return <div>Home</div>;
}
export default Home;
App.js sadeleştirildi.
import "./App.css";
import { Routes, Route, BrowserRouter as Router } from "react-router-dom";
import Home from "./pages/Home";
function App() {
return (
<>
<Router>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</Router>
</>
);
}
export default App;
charactersSlice.js içinde fetch edilen veri items değişkenine atandı. Bu kısımda loading ve error şartlarına uygun düzenleme de yapıldı.
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const char_limit = 12; // kaç karakter çekileceğini belirler
export const fetchCharacters = createAsyncThunk(
"characters/getCharacters",
async () => {
const res = await axios(
`${process.env.REACT_APP_API_BASE_ENDPOINT}/characters`
);
return res.data;
}
);
export const charactersSlice = createSlice({
name: "character",
initialState: {
items: [],
isLoading: false,
},
reducers: {},
extraReducers: {
[fetchCharacters.pending]: (state, action) => {
state.isLoading = true;
},
[fetchCharacters.fulfilled]: (state, action) => {
state.items = action.payload.slice(0, char_limit); // fetch edilen dosya statee geçildi. slice ile hangi aralığın statee geçileceği ayarlandı.
state.isLoading = false;
},
[fetchCharacters.rejected]: (state, action) => {
state.isLoading = false;
state.error = action.error.message;
},
},
});
export default charactersSlice.reducer;
redux içindeki veriler ve fetchCharacters fonksiyonu pages/Home/index.j içine import edilir.
Resimleri listelemek için react masonary css kullanacağız.
npm install react-masonry-css
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchCharacters } from "../../redux/charactersSlice";
import Masonry from "react-masonry-css"; // Çekilen verinin görsel kısmı için import edildi.
import Loading from "../../components/Loading";
import Error from "../../components/Error";
function Home() {
const characters = useSelector((state) => state.characters.items);
const isLoading = useSelector((state) => state.characters.isLoading);
const error = useSelector((state) => state.characters.error);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchCharacters());
}, [dispatch]);
if(isLoading){
return <Loading/>
}
if(error){
return <Error message={error}/>
}
return (
<div>
<Masonry
breakpointCols={4} // satırda kaç tane gösterileceğini belirler.
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
>
{characters.map((character) => (
<div key={character.id}>
<img src={character.image} alt={character.name} className="character"/>
<div className="char_name">{character.name}</div>
</div>
))}
</Masonry>
</div>
);
}
export default Home;
App.css içinde stil tanımları yapıldı. masonary-css için gereken tanımlar kendi sitesinden alındı.
.my-masonry-grid {
display: flex;
margin-left: -20px;
}
.my-masonry-grid_column {
border-left: 20px solid transparent;
background-clip: padding-box;
}
/* Style your items */
.my-masonry-grid_column > div { /* change div to reference your elements in <masonry> */
background: #ddd;
padding: 20px;
margin-bottom: 20px;
}
.character{
width: 100%;
}
.char_name{
padding: 10px 0;
font-size: 16px;
}
Home sayfasında kullanılmak için Error ve Loading kompanentleri oluşturuldu.
Loading.js
import React from "react";
function Loading() {
return <div style={{ padding: "10px" }}>Loading...</div>;
}
export default Loading;
Error.js
import React from "react";
function Error({ message }) {
return <div>Error: {message}</div>;
}
export default Error;
sayfalama işlemi için fetchCharacters asenkron fonksiyonunun parametre alması sağlandı. Buradan gelen page verisiyle .slice() fonksiyonunda her bir sayfa için sınırlı veri getirilmesi sağlandı.
initialState içinde page ve hasNextPage değişkenleri tanımlandı.
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const char_limit = 12; // kaç karakter çekileceğini belirler
export const fetchCharacters = createAsyncThunk(
"characters/getCharacters",
async (page) => {
const res = await axios(
`${process.env.REACT_APP_API_BASE_ENDPOINT}/characters`
);
return res.data.slice(page * char_limit, page * char_limit + char_limit);
}
);
export const charactersSlice = createSlice({
name: "character",
initialState: {
items: [],
isLoading: false,
page: 0, // sayfa numarasını değiştirebilmek için oluşturuldu
hasNextPage: true, // sonraki sayfa var mı?
},
reducers: {},
extraReducers: {
[fetchCharacters.pending]: (state, action) => {
state.isLoading = true;
},
[fetchCharacters.fulfilled]: (state, action) => {
state.items = [...state.items, ...action.payload]; // yeni gelen verileri eskisinin üzerine ekler.
state.isLoading = false;
state.page += 1; // her çalıştığında sayfa sayısını bir arttırır.
if (action.payload.length < char_limit) {
// son çekilen sayfadaki veri tanımlı karakter limitinden az ise hasNextPage false olur.
state.hasNextPage = false;
}
},
[fetchCharacters.rejected]: (state, action) => {
state.isLoading = false;
state.error = action.error.message;
},
},
});
export default charactersSlice.reducer;
İlgili veriler ve fonksiyonlar Home/index.js içinde import edildi ve kullanıldı.
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchCharacters } from "../../redux/charactersSlice";
import Masonry from "react-masonry-css"; // Çekilen verinin görsel kısmı için import edildi.
import Loading from "../../components/Loading";
import Error from "../../components/Error";
function Home() {
const characters = useSelector((state) => state.characters.items);
const page = useSelector((state) => state.characters.page);
const hasNextPage = useSelector((state) => state.characters.hasNextPage);
const isLoading = useSelector((state) => state.characters.isLoading);
const error = useSelector((state) => state.characters.error);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchCharacters(page));
}, [dispatch]);
if (error) {
return <Error message={error} />;
}
return (
<div>
<Masonry
breakpointCols={4}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
>
{characters.map((character) => (
<div key={character.id}>
<img
src={
character.image
? character.image
: "https://cdn.evrimagaci.org/zZKA_7o0hzCnltHvAhIaB9yWllk=/300x0/filters:format(webp)/evrimagaci.org%2Fpublic%2Fmi_media%2F1ffc457d5cd451f726f9287682746838.jpeg"
} // forograf bilgisi olmayanlar için yer tutucu tanımlandı.
alt={character.name}
className="character"
/>
<div className="char_name">{character.name}</div>
</div>
))}
</Masonry>
<div style={{ padding: "20px 0 40px 0", textAlign: "center" }}>
{isLoading && <Loading />}
{hasNextPage && !isLoading && (
<button onClick={() => dispatch(fetchCharacters(page))}>
{/* butona her tıklandığında mevcut page bilgisi fonksiyona gönderilir. Fonksiyon her başarıya ulaştığında charactersSlice.js içindeki fonksiyonunda sayfa sayısı bir artar */}
Load More ({page})
</button>
)}
{!hasNextPage && <div>There is nothing to be shown.</div>}
</div>
</div>
);
}
export default Home;
Test sırasında sayfanın her seferinde iki kez render edildiğini gözlemledik. Bu da sayfa sayısını ikişer ikişer arttırıyordu. Bunu engellemek için src/index.js aşağıdaki gibi düzenlendi.
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import { store } from "./redux/store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<> {/* değiştirilen kısım */}
<Provider store={store}>
<App />
</Provider>
</> // değiştirilen kısım
);
reportWebVitals();
App.js içinde gerekli routing işlemi tanımlandı.
import "./App.css";
import { Routes, Route, BrowserRouter as Router } from "react-router-dom";
import Home from "./pages/Home";
import Detail from "./pages/Detail";
function App() {
return (
<>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/char/:char_id" element={<Detail />} />
</Routes>
</Router>
</>
);
}
export default App;
Karakter fetch işlemini redux üzerinden yapabileceğimiz gibi direk kendi sayfasında da yazabiliriz. Bu örnekte kendi sayfasında yazdık. page/Detail/index.js içinde:
import axios from "axios";
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import Loading from "../../components/Loading";
function Detail() {
const [char, setChar] = useState(null);
const [loading, setLoading] = useState(true);
const { char_id } = useParams();
useEffect(() => {
axios(`${process.env.REACT_APP_API_BASE_ENDPOINT}/character/${char_id}`)
.then((res) => res.data)
.then((data) => setChar(data[0]))
.finally(setLoading(false));
}, []);
return (
<div>
{loading && <Loading />}
{char && (
<div>
<h1>{char.name} {char.alternate_names[0] && `(${char.alternate_names})`}</h1>
<img src={char.image} alt="" style={{ width: "50%" }} />
<pre>{JSON.stringify(char, null, 2)}</pre>
</div>
)}
</div>
);
}
export default Detail;
characterSlice.js içindeki isLoading tanımı bize sadece iki durum verdiği (true-false) ve buna bağlı olarak Home
sayfasına her dönüldüğünde fetch işlemi tekrarlandığı için bunun yerine status durumu yazıldı. status başkangıçta idle
yükleme sırasında loading
yükleme başarılı olunca succeeded
yükleme başarısız olursa failed
alır. Bu durum home sayfasındaki fetch işlemini status === "idle" koşuluna bağlamayı kolaylaştırır.
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const char_limit = 12; // kaç karakter çekileceğini belirler
export const fetchCharacters = createAsyncThunk(
"characters/getCharacters",
async (page) => {
const res = await axios(
`${process.env.REACT_APP_API_BASE_ENDPOINT}/characters`
);
return res.data.slice(page * char_limit, page * char_limit + char_limit);
}
);
export const charactersSlice = createSlice({
name: "character",
initialState: {
items: [],
status: "idle", // isLoading yerine status tanımı girildi. Başlangıç konumu "idle"
page: 0,
hasNextPage: true,
},
reducers: {},
extraReducers: {
[fetchCharacters.pending]: (state, action) => {
state.status = "loading"; //
},
[fetchCharacters.fulfilled]: (state, action) => {
state.items = [...state.items, ...action.payload];
state.status = "succeeded"; //
state.page += 1;
if (action.payload.length < char_limit) {
state.hasNextPage = false;
}
},
[fetchCharacters.rejected]: (state, action) => {
state.status = "failed"; //
state.error = action.error.message;
},
},
});
export default charactersSlice.reducer;
page/Home/index.js içinde fetch işlemi koşul altına alındı. Karakter kartları da link haline getirildi.
import { useEffect } from "react";
import Masonry from "react-masonry-css";
import Loading from "../../components/Loading";
import Error from "../../components/Error";
import { useSelector, useDispatch } from "react-redux";
import { fetchCharacters } from "../../redux/charactersSlice";
import { Link } from "react-router-dom";
function Home() {
const characters = useSelector((state) => state.characters.items);
const page = useSelector((state) => state.characters.page);
const hasNextPage = useSelector((state) => state.characters.hasNextPage);
const status = useSelector((state) => state.characters.status);
const error = useSelector((state) => state.characters.error);
const dispatch = useDispatch();
useEffect(() => {
if (status === "idle") {
dispatch(fetchCharacters(page));
}
}, [dispatch, status]);
if (status === "failed") {
return <Error message={error} />;
}
return (
<div>
<Masonry
breakpointCols={4}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
>
{characters.map((character) => (
<div key={character.id}>
<Link to={`/char/${character.id}`}>
<img
src={
character.image
? character.image
: "https://cdn.evrimagaci.org/zZKA_7o0hzCnltHvAhIaB9yWllk=/300x0/filters:format(webp)/evrimagaci.org%2Fpublic%2Fmi_media%2F1ffc457d5cd451f726f9287682746838.jpeg"
}
alt={character.name}
className="character"
/>
<div className="char_name">{character.name}</div>
</Link>
</div>
))}
</Masonry>
<div style={{ padding: "20px 0 40px 0", textAlign: "center" }}>
{status === "loading" && <Loading />}
{hasNextPage && status !== "loading" && (
<button onClick={() => dispatch(fetchCharacters(page))}>
Load More ({page})
</button>
)}
{!hasNextPage && <div>There is nothing to be shown.</div>}
</div>
</div>
);
}
export default Home;
App.css içine stil tanımları eklendi.
.my-masonry-grid {
display: flex;
margin-left: -20px;
}
.my-masonry-grid_column {
border-left: 20px solid transparent;
background-clip: padding-box;
}
/* Style your items */
.my-masonry-grid_column > div { /* change div to reference your elements in <masonry> */
margin-bottom: 20px;
}
.my-masonry-grid_column > div > a {
display: block;
padding: 20px;
color: #333;
text-decoration: none;
background: #ddd;
}
.my-masonry-grid_column > div > a:hover{
color: black;
background: #eee;
}
.character{
width: 100%;
}
.char_name{
padding: 10px 0;
font-size: 16px;
}
Bizim kullandığımız api içinde quotes yok Biz de başka kaynaktan aldık. https://api.quotable.io/quotes
Bu eğitimdeki stil tanımlarını da App.css içine ekledim. Bölümün en altına koydum.
App.js içinde navigasyon yapısı ve gereken routing işlemi tanımlandı.
import "./App.css";
import { Routes, Route, Link, BrowserRouter as Router } from "react-router-dom";
import Home from "./pages/Home";
import Detail from "./pages/Detail";
import Quotes from "./pages/Quotes";
function App() {
return (
<>
<Router>
<nav>
<ul>
<li>
<Link to="/">HP Characters</Link>
</li>
<li>
<Link to="/quotes">Quotes</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/char/:char_id" element={<Detail />} />
<Route path="/quotes" element={<Quotes />} />
</Routes>
</Router>
</>
);
}
export default App;
quotes verisinin alınması, tutulması ve işlenmesi için redux/quotesSlice.js dosyası oluşturuldu.
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const fetchAllQuotes = createAsyncThunk("quotes/fetchAll", async () => {
const res = await axios("https://api.quotable.io/quotes");
return res.data;
});
export const quotesSlice = createSlice({
name: "quotes",
initialState: {
items: [],
status: "idle",
},
reducers: {},
extraReducers: {
[fetchAllQuotes.pending]: (state, action) => {
state.status = "loading";
},
[fetchAllQuotes.fulfilled]: (state, action) => {
state.items = action.payload;
state.status = "succeeded";
},
[fetchAllQuotes.rejected]: (state, action) => {
state.error = action.error.message;
state.status = "failed";
},
},
});
export const quotesSelector = (state) => state.quotes.items;
export const statusSelector = (state) => state.quotes.status;
export const errorSelector = (state) => state.quotes.error;
export default quotesSlice.reducer;
redux/store.js içinde quotesSlice import edildi ve kullanıldı.
import { configureStore } from "@reduxjs/toolkit";
import charactersSlice from "./charactersSlice";
import quotesSlice from "./quotesSlice";
export const store = configureStore({
reducer: {
characters: charactersSlice,
quotes: quotesSlice,
},
});
pages/Quotes/index.js dosyası oluşturuldu ve quotesSlice içinden gelen veri kullanıldı.
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
errorSelector,
fetchAllQuotes,
quotesSelector,
statusSelector,
} from "../../redux/quotesSlice";
import Error from "../../components/Error";
import Loading from "../../components/Loading";
import Item from "./item";
function Quotes() {
const dispatch = useDispatch();
const data = useSelector(quotesSelector);
const status = useSelector(statusSelector);
const error = useSelector(errorSelector);
useEffect(() => {
if(status === "idle"){ // sayfanın her seferinde fetch edilmesini engeller.
dispatch(fetchAllQuotes());
}
}, [dispatch, status]);
if (error) {
return <Error message={error} />;
}
return (
<div style={{ padding: 10 }}>
<h1>Quotes</h1>
{status === "loading" && <Loading />}
{status === "succeeded" &&
data.results.map((item) => <Item key={item._id} item={item} />)}
{status === "succeeded" && <div className="quotes_info">{data.results.length} quotes in {data.totalCount} quotes</div>}
</div>
);
}
export default Quotes;
pages/Quotes/index.js sayfasında veri maplemek için kullanılan Item kompanenti pages/Quotes/Item.js içinde oluşturuldu.
import React from 'react'
function Item({item}) {
return (
<div className='quote_item'>
<q>{item.content}</q> <strong>{item.author}</strong>
</div>
)
}
export default Item
App.css içine eklenen stil tanımları
/* nav tanımları */
nav{
background-color: antiquewhite;
}
nav ul {
margin: 0 0 10px 0;
display: flex;
padding: 10px;
list-style-type: none;
}
nav ul li {
padding: 5px;
}
/* Item tanımları */
.quote_item {
padding: 10px 0 ;
}
.quotes_info {
padding: 30px;
text-align: center;
}
App.js içinde yönlendirme yazıldı.
import "./App.css";
import { Routes, Route, Link, BrowserRouter as Router } from "react-router-dom";
import Home from "./pages/Home";
import Detail from "./pages/Detail";
import Quotes from "./pages/Quotes";
import QuoteDetail from "./pages/QuoteDetail";
function App() {
return (
<>
<Router>
<nav>
<ul>
<li>
<Link to="/">HP Characters</Link>
</li>
<li>
<Link to="/quotes">Quotes</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/char/:char_id" element={<Detail />} />
<Route path="/quotes" element={<Quotes />} />
<Route path="/quotes/:quote_id" element={<QuoteDetail />} />
</Routes>
</Router>
</>
);
}
export default App;
Quotes/item.js içinde link verildi.
import React from "react";
import { Link } from "react-router-dom";
function Item({ item }) {
return (
<div className="quote_item">
<Link to={`/quotes/${item._id}`}>
<q>{item.content}</q>
</Link>{" "}{" "}
<strong>{item.author}</strong>
</div>
);
}
export default Item;
pages/QuoteDetail/index.js içinde veri fetch edildi ve kullanıldı.
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import axios from "axios";
import Loading from "../../components/Loading";
function QuoteDetail() {
const { quote_id } = useParams();
const [quote, setQuote] = useState();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
axios(`https://api.quotable.io/quotes/${quote_id}`)
.then((res) => res.data)
.then((data) => setQuote(data))
.finally(setIsLoading(false));
}, []);
return (
<div>
<h1>Quote Detail</h1>
{isLoading ? <Loading /> : <pre>{JSON.stringify(quote, null, 2)}</pre>}
</div>
);
}
export default QuoteDetail;
App.css içine stil tanımları eklendi.
/* Link kompanenti a tagı olarak render edilir. */
.quote_item a{
text-decoration: none;
color: #333;
}
.quote_item a:hover{
text-decoration: underline;
}
Son olarak alıntılar sayfasına da karakterler sayfsındaki gibi bir buton ekledim. İstediğim gibi çalışmadı.
Terminale
npx create-react-app .ile bulunduğumuz dizine yeni bir proje oluşturduk.
Terminale
npm install react-redux @reduxjs/toolkitile react-redux ve redux-toolkit i kurduk.
redux/store.js içinde store oluşturuldu.
import { configureStore } from "@reduxjs/toolkit";
import contactSlice from "./contactSlice";
export const store = configureStore({
reducer: {
contacts: contactSlice,
},
});
index.js içinde store import edildi ve import edilen Provider kompanenti ile kullanıldı.
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { store } from "./redux/store";
import { Provider } from "react-redux";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<>
<Provider store={store}>
<App />
</Provider>
</>
);
reportWebVitals();
Bu bölümde kullanacağımız createEntityAdaptor yapısı bizim daha önce oluşturduğumuz array içinde obje betodu yerine veriyi obje içinde unique id bir keye bağlı objeler olarak tutuyor. Ulaşmak gerektiğinde ise find() metodu kullanmadan, obje içindeki keye yönlenerek datayı bulmamızı sağlıyor. Bu metod kullanıldığında CRUD işlemleri için de hazır fonksiyonlara sahip oluyoruz. Detay okuma için tıklayınız.
redux/contactSlice.js içinde createEntityAdaptor yapısıyla birlikte bir slice yapısı kurgulandı.
import { createSlice, createEntityAdapter } from "@reduxjs/toolkit";
export const contactAdapter = createEntityAdapter(); //
const initialState = contactAdapter.getInitialState(); // bu tanım direk initialState karşısına da yazılabilir.
const contactSlice = createSlice({
name: "contacts",
initialState, //
reducers: {
addContact: contactAdapter.addOne, // addOne tanımı createEntityApadter içindeki hazır yeni eleman ekleme komutudur.
addContacts: contactAdapter.addMany // birden fazla elemanı aynı anda girmeye yarar.
},
});
export const { addContact, addContacts } = contactSlice.actions;
export default contactSlice.reducer;
componenets/Contacts/Form.js dosyası içinde contactSlice içinden alınan fonksiyonlar dispatch edilir ve kullanılır.
import React, { useState } from "react";
import { nanoid } from "@reduxjs/toolkit";
import { useDispatch } from "react-redux";
import { addContact, addContacts } from "../../redux/contactSlice";
function Form() {
const [name, setName] = useState("");
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (!name) return false;
const names = name.split(","); //"," ile ayrılmış bir sting veriyi "," e göre ayırır ve array yapar.
// names.forEach((name) => dispatch(addContact({ id: nanoid(), name }))); // arrayin her bir elemanını contact verisine ekler.
// yukarıdakinin doğru yolu: (yukarıda her eleman için ayrı ayrı işlem yapar. Aşağıda bunu tek işlemde yapar.)
const data = names.map((name) => ({ name, id: nanoid() })) // arraydeki ögeleri istenilen formata getirir.
dispatch(addContacts(data)); // arrayi tek hamlede ekler.
// dispatch(addContact({ id: nanoid(), name })); // verilen tek değeri ekler.
setName("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</form>
);
}
export default Form;
Bu kompanenti Contact/index.js içinde onu da App.js içinde kullandık.
Verinin kullanılacağı yerde çekilecek verinin özelliğini değiştirebilecek bazı özel fonksiyonların kullanılabilmesi için, redux/contactSlice.js içine aşağıdaki kod eklenir.
import { createSlice, createEntityAdapter } from "@reduxjs/toolkit";
export const contactAdapter = createEntityAdapter();
export const contactSelectors = contactAdapter.getSelectors( // Bu tanım selector ile veri çekerken bazı özel fonksiyonları kullanabilmemizi sağlar.
(state) => state.contacts
);
const initialState = contactAdapter.getInitialState();
const contactSlice = createSlice({
name: "contacts",
initialState,
reducers: {
addContact: contactAdapter.addOne,
addContacts: contactAdapter.addMany,
},
});
export const { addContact, addContacts } = contactSlice.actions;
export default contactSlice.reducer;
components/Contacts/List.js dosyası oluşturulur ve içine veriler çekilir.
import React from "react";
import { contactSelectors } from "../../redux/contactSlice";
import { useSelector } from "react-redux";
import Item from "./Item";
function List() {
const contacts = useSelector(contactSelectors.selectAll); // selectEntities: entities yapısı ile hepsini verir. selectAll array olarak verir. selectTotal kayıt sayısını verir.
console.log(contacts);
return <div>
{
contacts.map(contact => (<Item key={contact.id} item={contact}/>))
}
</div>;
}
export default List;
Listeleme işlemi için aynı klasörde List.js kompanenti oluşturulur.
import React from "react";
function Item({item}) {
return {item.name};
}
export default Item;
List kompanenti componenets/Contacts/index.js içinde kullanılır.
import React from "react";
import Form from "./Form";
import List from "./List";
function Contacts() {
return (
<div>
<h1>Contacts</h1>
<Form />
<List />
</div>
);
}
export default Contacts;
Form kompanentinden çoklu veri ekleme işlemi çıkartılarak kompanent sadeleştirilir.
import React, { useState } from "react";
import { nanoid } from "@reduxjs/toolkit";
import { useDispatch } from "react-redux";
import { addContact } from "../../redux/contactSlice";
function Form() {
const [name, setName] = useState("");
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (!name) return false;
dispatch(addContact({ id: nanoid(), name })); // verilen tek değeri ekler.
setName("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</form>
);
}
export default Form;
Daha önce buna benzer bir uygulama geliştirmiştik. Oradan aldığımız stil tanımları ile App.css dosyasını güncelledik.
.App {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
#container {
width: 400px;
background-color: aliceblue;
padding: 20px;
}
input {
width: 100%;
padding: 5px;
box-sizing: border-box; /* sağ sol hizalaması */
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
}
ul, li {
margin:0;
padding: 0;
list-style-type: none;
}
.list{
margin: 15px 0;
}
.list > li {
background-color: antiquewhite;
padding: 5px;
margin-bottom: 2px;
display: flex;
justify-content: space-between;
}
.btn {
display: flex;
justify-content: flex-end;
padding: 5px 0 ;
}
.btn > button {
padding: 5px;
width: 60px;
}
Form kompanenti içinde telefon numarası için input ekledik ve css için gerekli klas ve id bilgilerini ekledik.
import React, { useState } from "react";
import { nanoid } from "@reduxjs/toolkit";
import { useDispatch } from "react-redux";
import { addContact } from "../../redux/contactSlice";
function Form() {
const [name, setName] = useState("");
const [number, setNumber] = useState("");
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (!name || !number) return false;
dispatch(addContact({ id: nanoid(), name, phone_number: number }));
setName("");
setNumber("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
placeholder="phone number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<div className="btn">
<button type="submit">Add</button>
</div>
</form>
);
}
export default Form;
Aşağıdaki kompanentleri css tanımlarıyla uyumlu hale getirdik.
Contacts/index.js
import React from "react";
import Form from "./Form";
import List from "./List";
function Contacts() {
return (
<div id="container">
<h1>Contacts</h1>
<List />
<Form />
</div>
);
}
export default Contacts;
Contacts/List.js
import React from "react";
import { contactSelectors } from "../../redux/contactSlice";
import { useSelector } from "react-redux";
import Item from "./Item";
function List() {
const contacts = useSelector(contactSelectors.selectAll);
console.log(contacts);
return <ul className="list">
{
contacts.map(contact => (<Item key={contact.id} item={contact}/>))
}
</ul>;
}
export default List;
Contacts/Item.js
import React from "react";
function Item({item}) {
return <li>
<span>{item.name}</span>
<span>{item.phone_number}</span>
</li>;
}
export default Item;
contactSlice.js içinde tekli silme ve hepsini silme fonksiyonları tanımlandı.
import { createSlice, createEntityAdapter } from "@reduxjs/toolkit";
export const contactAdapter = createEntityAdapter();
export const contactSelectors = contactAdapter.getSelectors(
(state) => state.contacts
);
const initialState = contactAdapter.getInitialState();
const contactSlice = createSlice({
name: "contacts",
initialState,
reducers: {
addContact: contactAdapter.addOne,
addContacts: contactAdapter.addMany,
deleteContact: contactAdapter.removeOne, // bir tane veriyi silmek için kullanılır. Parametre olarak id değerini alır.
removeAllContacts: contactAdapter.removeAll // tüm verileri siler.
},
});
export const { addContact, addContacts, deleteContact, removeAllContacts } = contactSlice.actions;
export default contactSlice.reducer;
components/Contacts/item.js dosyasında silme butonu ve tekil silme fonksiyonu kullanıldı.
import React from "react";
import { useDispatch } from "react-redux";
import { deleteContact } from "../../redux/contactSlice";
function Item({ item }) {
const dispatch = useDispatch();
const handleDelete = (id) => {
if(window.confirm("Are you sure?")){
dispatch(deleteContact(id))
}
};
return (
<li>
<span>{item.name}</span>
<span>{item.phone_number}</span>
<span className="deleteBtn" onClick={()=>handleDelete(item.id)}>
x
</span>
</li>
);
}
export default Item;
components/Contacts/List.js içinte tüm veriyi silmek için gereken fonksiyon kullanıldı.
import React from "react";
import { contactSelectors, removeAllContacts } from "../../redux/contactSlice";
import { useSelector, useDispatch } from "react-redux";
import Item from "./Item";
function List() {
const contacts = useSelector(contactSelectors.selectAll);
const dispatch = useDispatch();
const handleDeleteAll = () => {
if (window.confirm("Are You Sure??")) {
dispatch(removeAllContacts());
}
};
return (
<div>
{contacts.length > 1 && (
<div className="deleteAllBtn" onClick={handleDeleteAll}>
Delete All
</div>
)}
<ul className="list">
{contacts.map((contact) => (
<Item key={contact.id} item={contact} />
))}
</ul>
</div>
);
}
export default List;
components/Contacts/index.js içinde var olan veri sayısı gösterildi.
import React from "react";
import Form from "./Form";
import List from "./List";
import { useSelector } from "react-redux";
import { contactSelectors } from "../../redux/contactSlice";
function Contacts() {
const total = useSelector(contactSelectors.selectTotal)
return (
<div id="container">
<h1>Contacts ({total})</h1>
<List />
<Form />
</div>
);
}
export default Contacts;
App.css içine stil tanımları eklendi.
.deleteBtn{
width: 18px;
height: 22px;
background-color: red;
color: white;
text-align: center;
cursor: pointer;
padding: 5px;
}
.deleteAllBtn{
text-align: right;
cursor: pointer;
font-size: 12px;
}
.deleteAllBtn:hover{
text-decoration: underline;
}
contactSlice içinde update fonksiyon yazıldı.
import { createSlice, createEntityAdapter } from "@reduxjs/toolkit";
export const contactAdapter = createEntityAdapter();
export const contactSelectors = contactAdapter.getSelectors(
(state) => state.contacts
);
const initialState = contactAdapter.getInitialState();
const contactSlice = createSlice({
name: "contacts",
initialState,
reducers: {
addContact: contactAdapter.addOne,
addContacts: contactAdapter.addMany,
deleteContact: contactAdapter.removeOne,
removeAllContacts: contactAdapter.removeAll,
updateContact: contactAdapter.updateOne, // veri güncellemek için kullanılır. ilk parametre olarak id, ikinci parametre olarak changes keyi ile değişen değerleri obje olarak alır.
},
});
export const {
addContact,
addContacts,
deleteContact,
removeAllContacts,
updateContact,
} = contactSlice.actions;
export default contactSlice.reducer;
App.js içinde Edit kompanentine yönlendirme yapıldı.
import "./App.css";
import Contacts from "./components/Contacts";
import Edit from "./components/Contacts/Edit";
import { Routes, Route, BrowserRouter as Router } from "react-router-dom";
function App() {
return (
<div className="App">
<div id="container">
<Router>
<Routes>
<Route path="/" element={<Contacts />} />
<Route path="/edit/:id" element={<Edit />} />
</Routes>
</Router>
</div>
</div>
);
}
export default App;
Item komponenti içinde Edit komponentine link verildi.
import React from "react";
import { useDispatch } from "react-redux";
import { deleteContact } from "../../redux/contactSlice";
import { Link } from "react-router-dom";
function Item({ item }) {
const dispatch = useDispatch();
const handleDelete = (id) => {
if (window.confirm("Are you sure?")) {
dispatch(deleteContact(id));
}
};
return (
<li>
<span>{item.name}</span>
<span>{item.phone_number}</span>
<div className="edit">
<Link to={`/edit/${item.id}`}>
<span>Edit</span>
</Link>
<span className="deleteBtn" onClick={() => handleDelete(item.id)}>
x
</span>
</div>
</li>
);
}
export default Item;
Edit kompanenti oluşturuldu ve içinde useParams kullanılarak id, id kullanılarak da id ye ait contact bilgisine ulaştık.
import React from "react";
import { useParams, Navigate } from "react-router-dom";
import EditForm from "./EditForm";
import { useSelector } from "react-redux";
import { contactSelectors } from "../../redux/contactSlice";
function Edit() {
const { id } = useParams();
const contact = useSelector((state) =>
contactSelectors.selectById(state, id)
);
if (!contact) { // sayfa yenilendiğinde contact verisi gidiyor. Bu durumda da hataya düşüyordu. Biz de bu koşulu yazdık
return <Navigate to={"/"} />;
}
return (
<div>
<h1>Edit</h1>
<EditForm contact={contact} />
</div>
);
}
export default Edit;
Edit komponentinde kullanılan EditForm komponenti oluşturuldu.
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { updateContact } from "../../redux/contactSlice";
import { useNavigate } from "react-router-dom"; // yönlendirmeyi fonksiyon içinde yapmak için bu hook çekildi.
function EditForm({ contact }) {
const [name, setName] = useState(contact.name);
const [number, setNumber] = useState(contact.phone_number);
const dispatch = useDispatch();
const navigate = useNavigate(); // yönlendirme hooku değişkene tanımlandı
const handleSubmit = (e) => {
e.preventDefault();
if (!name || !number) return false;
dispatch(
updateContact({
id: contact.id,
changes: {
name,
phone_number: number,
},
})
);
navigate("/") // işlem sonunda ana sayfaya yönlendirildi.
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
placeholder="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
placeholder="phone number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<div className="btn">
<button type="submit">Update</button>
</div>
</form>
</div>
);
}
export default EditForm;
stil amaçlo olarak, Contacts/index.js kompanentindeki <div id="container"> ifadesi App.js içine alındı.
App.css içine aşağıdaki tanımlar eklendi.
.edit span{
margin-left: 10px;
font-size: 12px ;
cursor: pointer;
}