Redux  —  это популярная библиотека предсказуемого и последовательного управления состоянием с простой разработкой и сопровождением сложных приложений JavaScript.

Реализуем Redux на Rust, производительном и безопасном статически типизированном языке системного программирования.

Для этого создадим структуру, которой обозначается состояние приложения, и типаж для определения выполняемых с этим состоянием действий. И еще функцию редьюсера, которой принимается текущее состояние и действие, а возвращается обновленное этим действием состояние.

Базовая реализация

Вот простая структура, которой обозначается состояние приложения списка задач:

struct TodoState {
todos: Vec<String>,
}

Определим действия, выполняемые над состоянием с помощью типажа:

trait TodoAction {
fn apply(&self, state: &mut TodoState);
}

И функцию редьюсера, которой принимается текущее состояние и действие, а возвращается обновленное этим действием состояние:

fn todo_reducer(state: &TodoState, action: &dyn TodoAction) -> TodoState {
let mut new_state = state.clone();
action.apply(&mut new_state);
new_state
}

Теперь определим конкретные действия, выполняемые над состоянием приложения, например добавление новых задач:

struct AddTodoAction {
todo: String,
}

impl TodoAction for AddTodoAction {
fn apply(&self, state: &mut TodoState) {
state.todos.push(self.todo.clone());
}
}

И удаление старых:

struct RemoveTodoAction {
index: usize,
}

impl TodoAction for RemoveTodoAction {
fn apply(&self, state: &mut TodoState) {
state.todos.remove(self.index);
}
}

Используя функцию редьюсера, обновим состояние приложения после взаимодействия с пользователем. Например, если пользователь добавляет новый список задач, обновляем состояние функцией todo_reducer:

let mut state = TodoState { todos: vec![] };
let action = AddTodoAction { todo: "Learn Rust".to_string() };
state = todo_reducer(&state, &action);

Эта простая реализация Redux на Rust хороша для понимания базовой структуры и того, как создаются действия и функция редьюсера.

Имеется много других реализаций, в том числе с макросом для генерирования функции редьюсера и перечислениями для обозначения действий.

Макрос и перечисления

Сначала определим перечисление для действий, выполняемых с состоянием:

enum TodoAction {
AddTodo(String),
RemoveTodo(usize),
}

И макрос create_reducer для функции редьюсера:

#[macro_export]
macro_rules! create_reducer {
($state_type:ty, $action_type:ty, $reducer_fn:expr) => {
fn reducer(state: &$state_type, action: $action_type) -> $state_type {
let mut new_state = state.clone();
$reducer_fn(&mut new_state, action);
new_state
}
}
}

Определим ее с помощью этого же макроса:

create_reducer!(TodoState, TodoAction, |state: &mut TodoState, action| {
match action {
TodoAction::AddTodo(todo) => state.todos.push(todo),
TodoAction::RemoveTodo(index) => state.todos.remove(index),
}
});

В сгенерированной функции редьюсера принимаются TodoState и перечисление TodoAction и возвращается обновленное действием состояние.

Обновим ею состояние приложения:

let mut state = TodoState { todos: vec![] };
let action = TodoAction::AddTodo("Learn Rust".to_string());
state = reducer(&state, action);

Создаем пользовательский интерфейс

Сначала определим компонент  —  отдельный элемент приложения:

use yew::{html, Callback, Html};

struct TodoItem {
todo: String,
on_remove: Callback<()>,
}

impl Component for TodoItem {
type Message = ();
type Properties = Self;

fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self {
todo: props.todo,
on_remove: props.on_remove,
}
}

fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}

fn view(&self) -> Html {
html! {
<div>
<span>{ &self.todo }</span>
<button onclick=self.on_remove.clone()>{"Remove"}</button>
</div>
}
}
}

Затем основной компонент:

use yew::{html, Callback, Component, ComponentLink, Html, ShouldRender};
use yew_functional::{use_state, use_reducer};

struct TodoApp {
link: ComponentLink<Self>,
state: TodoState,
dispatch: Callback<TodoAction>,
}

impl Component for TodoApp {
type Message = ();
type Properties = ();

fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
let state = TodoState { todos: vec![] };
let (dispatch, _) = use_reducer(link, reducer, state);
Self { link, state, dispatch }
}

fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}

fn view(&self) -> Html {
html! {
<div>
<h1>{"Todo List"}</h1>
<ul>
{ for self.state.todos.iter().enumerate().map(|(index, todo)| {
html! {
<TodoItem
todo=todo.clone()
on_remove=self.link.callback(move |_| {
self.dispatch.emit(TodoAction::RemoveTodo(index))
})
/>
}
}) }
</ul>
<form onsubmit=self.link.callback(|event| {
event.prevent_default();
let input = event.target().unwrap().try_into::<HtmlInputElement>().unwrap();
let todo = input.value();
input.set_value("");
self.dispatch.emit(TodoAction::AddTodo(todo))
})>
<input type="text" />
<button type="submit">{"Add Todo"}</button>
</form>
</div>
}
}

Им отображаются список элементов и форма для добавления новых задач. Состояние приложения управляется хуком use_reducer из Yew и после взаимодействия с пользователем обновляется с помощью определенной выше функции редьюсера.

Для использования в приложении Yew добавим этот компонент в корневой:

use yew::{html, App};

fn main() {
yew::initialize();
App::<TodoApp>::new().mount_to_body();
yew::run_loop();
}

Так на Rust создается веб-приложение с Yew и Redux.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Radovan Stevanovic: Implementing Redux With Rust

Предыдущая статьяЖиви и программируй: обретение баланса
Следующая статьяКак выглядит нескучный модульный лендинг React