Preact Signals: React’te Re-render Sorunlarına Çözüm

Mustafa Morbel
Cimri Engineering
Published in
6 min readNov 15, 2023

--

React’teki hookları doğru kullanmak biraz karmaşık (useMemo, useCallback gibi), üstelik bunları performanslı bir şekilde kullanmak daha da zor. Bu durum, birçok uygulamamızın kod kalitesini düşürdü ve performansını olumsuz etkiledi. Ancak artık durum böyle olmak zorunda değil.

Render Performansı ve Virtual DOM:

Geleneksel Virtual DOM tabanlı frameworkler, bir state geçersizliğinden etkilenen tüm ağacı güncellemek zorundadır. Temelde, render performansı, o ağaçtaki bileşenlerin sayısına bağlıdır. Bileşen ağacının parçalarını memo veya useMemo kullanarak önbelleğe almak suretiyle bu sorunun etrafından dolaşabiliriz. Eğer hiçbir şey değişmemişse, bu, çerçevenin ağacın bazı kısımlarının render edilmesini atlamasına olanak tanır.

Teoride Mantıklı, Pratikte Karmaşık:

Teoride bu yaklaşım makul görünse de, pratikte kod tabanları büyüdükçe bu optimizasyonların nereye yerleştirileceğini belirlemek zorlaşır. Sık sık, iyi niyetle yapılan önbelleğe alma işlemleri, kararsız bağımlılık değerleri nedeniyle etkisiz hale gelebilir.

Signals, Re-render Problemleri İçin Güzel Bir Çözüm:

Signals, re-render sorunlarına karşı etkili bir çözüm sunar. Observer olarak çalışır ve subscribe olan tüm bileşenlerde hızlıca güncelleme yapabilir. Üstelik HTML’i yeniden oluşturmaya gerek duymaz. Virtual DOM üzerinde çalışır ve state üzerinde tutulan değişken güncellendiğinde, yeni versiyonun bir farkını oluşturup sadece ilgili kısmı değiştirerek güncelleme yapar.

Bu özellik, özellikle büyük ve karmaşık uygulamalarda performans artışı sağlayabilir. Hem de kullanımı oldukça basittir.

Nasıl Çalışır?

Signals, bir değişkenin güncellenmesini izler ve bu değişiklikleri subscribe olan tüm bileşenlere ileterek güncelleme yapar. Bu, değişikliklerin hızlı bir şekilde dağıtılmasını sağlar ve gereksiz yeniden çizimleri önler.

Temel Kullanım:

  • Signals, bir .value özelliğine sahip nesnelerdir.
  • Bir signal’in değerini okuyabilir ve güncelleyebilirsiniz. Bir signal’in .value'su güncellendiğinde, kullanıldığı bileşenin yeniden çizilmesini tetikler.
import { signal } from "@preact/signals";
const count = signal(0);
console.log(count.value); // Çıktı: 0
count.value += 1;
console.log(count.value); // Çıktı: 1

Component ile Kullanım:

  • Signals, component ağacı boyunca props veya context aracılığıyla iletilerek kullanılabilir.
  • Component içinden bir signal’in .value (değerine) erişmek, signal’in değeri değiştiğinde componentin otomatik olarak yeniden çizimini sağlar.
import { signal } from "@preact/signals";

// Abone olunabilir bir signal oluşturun:
const count = signal(0);

function Counter() {
// Değiştiğinde otomatik olarak yeniden oluşturulan .value'ya erişme:
const value = count.value;

const increment = () => {
// Bir signal `.value` özelliğine atayarak güncelleme:
count.value++;
}

return (
<div>
<p>Count: {value}</p>
<button onClick={increment}>click me</button>
</div>
);
}

JSX ile Kullanım:

Signals, en iyi performans sağlamak için Preact’e derinlemesine entegre edilmiştir. Yukarıdaki örnekte, count signalinin mevcut değerini almak için count.value’ya eriştik, ancak bu gereksizdir. Bunun yerine, Preact’e count signalini JSX içinde doğrudan kullanmasına izin verebiliriz:

import { signal } from "@preact/signals";

const count = signal(0);

function Counter() {
return (
<div>
<p>Count: {count}</p>
<button onClick={() => count.value++}>click me</button>
</div>
);
}

Signals ile Basit Bir Uygulama

Projeye Eklenmesi

Signals, React projenizde veya Preact projenizde doğrudan kullanılabilir.
Signalleri kullanmaya başlamak için @preact/signals paketini projenize ekleyerek kurabilirsiniz:

npm install @preact/signals

Şimdi gerçek bir senaryoda kullanalım. Bir yapılacaklar listesi uygulaması oluşturacağız; öğeleri ekleyip kaldırabileceğiniz bir yapılacaklar listesi. İlk olarak, durumu modellememiz gerekecek.

import { signal } from "@preact/signals";

const todos = signal([
{ text: "Market alışverişi" },
{ text: "Ev Temizliği" },
]);

Yeni bir yapılacak öğesi için kullanıcıdan metin girmesini istemek için daha sonra bir <input> öğesine bağlayacağımız bir signale daha ihtiyacımız olacak. Şu anda bu signali kullanarak listemize bir yapılacak öğesi ekleyen bir fonksiyon oluşturabiliriz. Unutmayın, bir signalin değerini .value özelliğine atayarak güncelleyebilirsiniz:

// Daha sonra <input> ile bağlayacağımız bir signali kullanmak için
const text = signal("");

function addTodo() {
todos.value = [...todos.value, { text: text.value }];
text.value = ""; // Ekleme işlemi sonrasında giriş değerini temizle
}

Bir signale yalnızca yeni bir değer atadığınızda güncellenir. Bir signale atadığınız değer, signalin mevcut değerine eşitse, güncellenmez.

const count = signal(0);

count.value = 0; // hiçbir işlem yapmaz - değer zaten 0
count.value = 1; // güncellenir - değer farklı

text signalini günceller ve addTodo() fonksiyonunu çağırırsak, bir yeni öğenin todos signaline eklenmesi gerektiğini görmeliyiz. Bu fonksiyonları doğrudan çağırarak bu senaryoyu simüle edebiliriz — henüz bir kullanıcı arayüzüne ihtiyacımız yok!

import { signal } from "@preact/signals";

const todos = signal([
{ text: "Market alışverişi" },
{ text: "Ev Temizliği" },
]);

const text = signal("");

function addTodo() {
todos.value = [...todos.value, { text: text.value }];
text.value = ""; // Ekleme işlemi sonrasında giriş değerini temizle
}

// Mantığımızın doğru olup olmadığını kontrol et
console.log(todos.value);
// Kaydedildi: [{text: "Market alışverişi"}, {text: "Ev Temizliği"}]

// Yeni bir yapılacak eklemeyi simüle et
text.value = "Düzenle";
addTodo();

// Yeni öğenin eklenip 'text' signalinin temizlendiğini kontrol et:
console.log(todos.value);
// Kaydedildi: [{text: "Market alışverişi"}, {text: "Ev Temizliği"}, {text: "Düzenle"}]

console.log(text.value); // Kaydedildi: ""

Son eklemek istediğimiz özellik, listeden bir yapılacak öğesini kaldırma yeteneğidir. Bunun için todos dizisinden belirli bir yapılacak öğesini kaldıran bir fonksiyon ekleyeceğiz:

function removeTodo(todo) {
todos.value = todos.value.filter(t => t !== todo);
}

UI Oluşturma:

Şimdi uygulamamızın durumunu modelledik, kullanıcıların etkileşimde bulunabileceği bir UI ile bağlayalım.

function TodoList() {
const onInput = event => (text.value = event.target.value);

return (
<>
<input value={text.value} onInput={onInput} />
<button onClick={addTodo}>Ekle</button>
<ul>
{todos.value.map(todo => (
<li>
{todo.text}{' '}
<button onClick={() => removeTodo(todo)}>kaldır</button>
</li>
))}
</ul>
</>
);
}

Computed Signals ile State Türetme:

Yapılacaklar uygulamamıza bir özellik ekleyelim: Her bir yapılacak öğe tamamlandığında işaretlenebilir ve kullanıcıya kaç öğe tamamladıklarını gösteririz. Bunun için, başka signallerden türetilen bir signal oluşturmak için computed(fn) fonksiyonunu içe aktaracağız. Bu fonksiyon, callback fonksiyonu içindeki diğer signallerin değerlerine dayalı olarak hesaplanan yeni bir signal oluşturur. Dönen computed signali salt okunur (read-only) olup, callback fonksiyonu içinden erişilen herhangi bir signalin değişmesi durumunda değeri otomatik olarak güncellenir.

import { signal, computed } from "@preact/signals";

const todos = signal([
{ text: "Market alışverişi", completed: true },
{ text: "Ev Temizliği", completed: false },
]);

// Diğer signallerden türetilmiş bir signal oluştur
const completed = computed(() => {
// `todos` değiştiğinde, bu otomatik olarak yeniden çalışır:
return todos.value.filter(todo => todo.completed).length;
});

// Kaydedildi: 1, çünkü bir yapılacak öğe tamamlandı olarak işaretlendi
console.log(completed.value);

Global State Management:

Şu ana kadar, signalleri yalnızca bileşen ağacının dışında oluşturduk. Bu, bir todo listesi gibi küçük bir uygulama için iyidir, ancak daha büyük ve karmaşık uygulamalarda bu test yapmayı zorlaştırabilir. Testler genellikle belirli bir senaryoyu çoğaltmak için uygulama durumundaki değerleri değiştirmeyi ve ardından bu durumu bileşenlere iletmeyi içerir. Bunun için uygulama durumumuzu bir işlev içine çıkarabiliriz:

function createAppState() {
const todos = signal([]);

// todos değerinin değişmesi durumunda otomatik olarak güncellenecek
const completed = computed(() => {
return todos.value.filter(todo => todo.completed).length
});

return { todos, completed }
}

Artık uygulama state’ini bir özellik olarak geçerek render sırasında iletebiliriz:

const state = createAppState();

// ...daha sonra:
<TodoList state={state} />

Bu, uygulama state’i global olduğunda çalışır, Durumu manuel olarak props aracılığıyla her bileşene geçirmemek için, durumu Context’e yerleştirerek herhangi bir bileşenin bu duruma erişebilmesini sağlayabiliriz.

import { createContext } from "preact";
import { useContext } from "preact/hooks";
import { createAppState } from "./my-app-state";

const AppState = createContext();

render(
<AppState.Provider value={createAppState()}>
<App />
</AppState.Provider>
);

// ...daha sonra uygulama durumuna erişme ihtiyacı olduğunuzda
function App() {
const state = useContext(AppState);
return <p>{state.completed}</p>;
}

Signals Local State:

Uygulama state’inin çoğu genellikle props ve context aracılığıyla geçer. Bununla birlikte, bazı durumlarda bileşenlerin kendi local state’ine ihtiyaç duyduğu senaryolar vardır. Bu durumda, useSignal() ve useComputed() kancalarını kullanarak bileşenler içinde doğrudan signalleri ve türetilmiş signalleri oluşturabiliriz:

import { useSignal, useComputed } from "@preact/signals";

function Counter() {
const count = useSignal(0);
const double = useComputed(() => count.value * 2);

return (
<div>
<p>{count} x 2 = {double}</p>
<button onClick={() => count.value++}>Ekle</button>
</div>
);
}

💡 Arkadaki implementasyon şu şekildedir:

function useSignal(value) {
return useMemo(() => signal(value), []);
}

Preact Signals ile React uygulamalarınızı daha performanslı hale getirebilirsiniz. Daha detaylı bilgi için aşağıdaki linkleri takip edebilirsiniz.

Buraya kadar okuduğunuz için teşekkür ederim.

--

--