vstore 是用于 vecty 框架的类似 redux 的状态管理库。
安装
go get marwan.io/vstore
代码仓库: https://github.com/marwan-at-work/vstore
定义 action
一般使用结构定义,比如
type Increment struct{}
和
type AddTodo struct {
Id int
Text string
}
定义 state 和 reducer
比如 todo app 的 state 定义
type State struct {
Todos []*Todo
Filter Filter
}
要实现 vstore.Reducer 接口,比如
func (s *State) Reduce(action interface{}) {
switch a := action.(type) {
case *AddTodo:
s.Todos = append(s.Todos, &Todo{
Id: a.Id,
Completed: false,
Text: a.Text,
})
case *SetVisibilityFilter:
s.Filter = a.Filter
case *ToggleTodo:
println("reduce toggle todo:", a.Id)
for _, todo := range s.Todos {
if todo.Id == a.Id {
todo.Completed = !todo.Completed
}
}
}
}
使用 switch action.(type) {} 的控制结构来区别处理不同 action。
vstore 的 Reduce 方法不同于 redux 中的 reduce 函数定义 (previousState, action) => newState, vstore 的 Reduce 方法要直接修改 state,而不是创建新的 state。
定义组件
组件需要实现 vstore.StoreComponent 接口,即提供 Connect 方法,一般在 Connect 方法中获取新的 state,然后修改自身的属性的值。
在组件的上一级组件的 Render 方法中,要调用 store.Connect 方法包装该组件。
比如
type Comp1 struct {
v.Core
store vstore.Store
}
func (*c Comp1) Render() {
return elem.Div(
c.store.Connect(&Comp2{}),
)
}
type Comp2 struct {
v.Core
Text string
}
func (c *Comp2) Connect(store vstore.Store) {
state := store.State().(*State)
c.Text = state.Text
}
func (*c Comp2) Render() {
return elem.Paragraph(
v.Text(c.Text),
)
}
完整例子
package main
import (
"strings"
v "github.com/gopherjs/vecty"
"github.com/gopherjs/vecty/elem"
"github.com/gopherjs/vecty/event"
"github.com/gopherjs/vecty/prop"
"marwan.io/vstore"
)
func main() {
state := &State{}
state.Filter = FilterAll
nextTodoId = 2
state.Todos = []*Todo{
{
Id: 1,
Text: "todo 1",
},
{
Id: 2,
Text: "todo 2",
},
}
store := vstore.New(state)
b := &body{
store: store,
}
v.RenderBody(b)
}
// actions
type AddTodo struct {
Text string
Id int
}
var nextTodoId = 0
func addTodo(text string) *AddTodo {
nextTodoId++
return &AddTodo{
Id: nextTodoId,
Text: text,
}
}
type Filter int
const (
FilterAll = iota
FilterCompleted
FilterActive
)
type SetVisibilityFilter struct {
Filter Filter
}
type ToggleTodo struct {
Id int
}
type State struct {
Todos []*Todo
Filter Filter
}
type Todo struct {
Id int // ???
Completed bool
Text string
}
func (s *State) Reduce(action interface{}) {
switch a := action.(type) {
case *AddTodo:
s.Todos = append(s.Todos, &Todo{
Id: a.Id,
Completed: false,
Text: a.Text,
})
case *SetVisibilityFilter:
s.Filter = a.Filter
case *ToggleTodo:
println("reduce toggle todo:", a.Id)
for _, todo := range s.Todos {
if todo.Id == a.Id {
todo.Completed = !todo.Completed
}
}
}
}
type TodoView struct {
v.Core
Id int
Completed bool `vecty:"prop"`
Text string `vecty:"prop"`
OnClick func(e *v.Event)
}
func (tv *TodoView) Key() interface{} {
return tv.Id
}
func (tv *TodoView) Render() v.ComponentOrHTML {
textDecoration := "none"
if tv.Completed {
textDecoration = "line-through"
}
return elem.ListItem(
v.Markup(
event.Click(tv.OnClick),
v.Style("text-decoration", textDecoration),
),
v.Text(tv.Text),
)
}
type TodoListView struct {
v.Core
TodoList []*Todo `vecty:"prop"`
store vstore.Store
}
func (tlv *TodoListView) Connect(store vstore.Store) {
tlv.store = store
state := store.State().(*State)
var todoList []*Todo
for _, todo := range state.Todos {
add := false
switch state.Filter {
case FilterActive:
if !todo.Completed {
add = true
}
case FilterCompleted:
if todo.Completed {
add = true
}
case FilterAll:
add = true
}
if add {
todoList = append(todoList, todo)
}
}
tlv.TodoList = todoList
}
func (tlv *TodoListView) Render() v.ComponentOrHTML {
var todoList v.List
for _, todo := range tlv.TodoList {
id := todo.Id
todoList = append(todoList, &TodoView{
Id: todo.Id,
Completed: todo.Completed,
Text: todo.Text,
OnClick: func(e *v.Event) {
println("on todo click", id)
tlv.store.Dispatch(&ToggleTodo{id})
},
})
}
return elem.UnorderedList(
todoList,
)
}
type Link struct {
v.Core
Active bool `vecty:"prop"`
store vstore.Store
Text string `vecty:"prop"`
Filter Filter
}
func (l *Link) Connect(store vstore.Store) {
l.store = store
state := store.State().(*State)
l.Active = state.Filter == l.Filter
}
func (l *Link) Render() v.ComponentOrHTML {
if l.Active {
return elem.Span(v.Text(l.Text))
}
return elem.Anchor(
v.Markup(
prop.Href(""),
event.Click(func(e *v.Event) {
l.store.Dispatch(&SetVisibilityFilter{l.Filter})
}).PreventDefault(),
),
v.Text(l.Text),
)
}
type Footer struct {
v.Core
store vstore.Store
}
func (f *Footer) Render() v.ComponentOrHTML {
return elem.Paragraph(
v.Text("Show:"),
f.store.Connect(&Link{
Filter: FilterAll,
Text: "All",
}),
f.store.Connect(&Link{
Filter: FilterActive,
Text: "Active",
}),
f.store.Connect(&Link{
Filter: FilterCompleted,
Text: "Completed",
}),
)
}
type AddTodoView struct {
v.Core
store vstore.Store
}
func (atv *AddTodoView) Render() v.ComponentOrHTML {
input := elem.Input()
return elem.Div(
elem.Form(
v.Markup(
event.Submit(func(e *v.Event) {
}).PreventDefault(),
),
input,
elem.Button(
v.Markup(
prop.Type("submit"),
event.Click(func(e *v.Event) {
println("btn click")
node := input.Node()
value := strings.TrimSpace(node.Get("value").String())
if value != "" {
atv.store.Dispatch(addTodo(value))
node.Set("value", "")
}
}),
),
v.Text("Add Todo"),
),
),
)
}
type body struct {
v.Core
store vstore.Store
}
func (b *body) Render() v.ComponentOrHTML {
return elem.Body(
&AddTodoView{
store: b.store,
},
b.store.Connect(&TodoListView{}),
&Footer{
store: b.store,
},
)
}