Need help setting up sync of signals backed by GraphQL, details in a thread

GitHub
GitHub - nikitavoloboev/kuskus: Fast todo app
Fast todo app. Contribute to nikitavoloboev/kuskus development by creating an account on GitHub.
45 Replies
nikivi
nikiviOP2y ago
above code is for todo app, it works great with just signals
nikivi
nikiviOP2y ago
GitHub
kuskus/store.tsx at main · nikitavoloboev/kuskus
Fast todo app. Contribute to nikitavoloboev/kuskus development by creating an account on GitHub.
nikivi
nikiviOP2y ago
GitHub
kuskus/store.tsx at main · nikitavoloboev/kuskus
Fast todo app. Contribute to nikitavoloboev/kuskus development by creating an account on GitHub.
nikivi
nikiviOP2y ago
onMount it loads things into the main signal todos
nikivi
nikiviOP2y ago
I am using https://grafbase.com as a way to persist data in a db but it shouldn't matter what I use
Grafbase
The unified data layer
Combine your data sources into a centralized GraphQL endpoint
nikivi
nikiviOP2y ago
here is my approach and what I want to achieve 1. user loads app, one big request to get all todos etc. is done, data saved into signals 2. user does things inside app, mutates signals 3. there is listener on signals that if signal changes, it sends a mutation to grafbase im just not sure how to do best do this, maybe createEffect inside the createContextProvider
nikivi
nikiviOP2y ago
nikivi
nikiviOP2y ago
for example I can do this and it would work basically when I mutate the signal todos I also send the mutation to the db but ideally and this would be so dreamy if I can make it work
nikivi
nikiviOP2y ago
nikivi
nikiviOP2y ago
I have this effect inside the store.tsx which holds my global signals so when todos() ever changes it will also persists it to db issue with above case is that that effect would run on first mount too amongst other issues potentially 🤔
nikivi
nikiviOP2y ago
nikivi
nikiviOP2y ago
GitHub
kuskus/store.tsx at main · nikitavoloboev/kuskus
Fast todo app. Contribute to nikitavoloboev/kuskus development by creating an account on GitHub.
nikivi
nikiviOP2y ago
the code I have now for store
nikivi
nikiviOP2y ago
nikivi
nikiviOP2y ago
I can guard against first mount like this in theory
export const [GlobalContextProvider, useGlobalContext] = createContextProvider(
() => {
const [todos, setTodos] = createSignal<TodoType[]>([])
const [mounted, setMounted] = createSignal(false)
onMount(async () => {
const res = await grafbase.request<Query>(TodosDocument)
if (res.todoCollection?.edges) {
res.todoCollection.edges.map((todo) => {
setTodos([...todos(), todo?.node as TodoType])
})
}
setMounted(true)
})
export const [GlobalContextProvider, useGlobalContext] = createContextProvider(
() => {
const [todos, setTodos] = createSignal<TodoType[]>([])
const [mounted, setMounted] = createSignal(false)
onMount(async () => {
const res = await grafbase.request<Query>(TodosDocument)
if (res.todoCollection?.edges) {
res.todoCollection.edges.map((todo) => {
setTodos([...todos(), todo?.node as TodoType])
})
}
setMounted(true)
})
fixed the annoying type error too ahh it still runs the mutation when i first load the page 😦 I basically don't want to run the effect on first load
nikivi
nikiviOP2y ago
nikivi
nikiviOP2y ago
I kind of want some api that i can wrap over signal updates so it does not trigger the effects that would solve my issue i think
export const [GlobalContextProvider, useGlobalContext] = createContextProvider(
() => {
const [todos, setTodos] = createSignal<TodoType[]>([])
const [mounted, setMounted] = createSignal(false)
onMount(async () => {
const res = await grafbase.request<Query>(TodosDocument)
if (res.todoCollection?.edges) {
res.todoCollection.edges.map((todo) => {
setTodos([...todos(), todo?.node as TodoType])
})
}
setMounted(true)
})

createEffect(async () => {
if (!mounted()) {
untrack(async () => {
if (todos().length > 0) {
const res = await grafbase.request<Mutation>(TodoUpdateDocument, {
id: "todo_01GYYYEEMR8EF1CC8F8AA0N29F",
title: "....",
})
}
})
}
})
export const [GlobalContextProvider, useGlobalContext] = createContextProvider(
() => {
const [todos, setTodos] = createSignal<TodoType[]>([])
const [mounted, setMounted] = createSignal(false)
onMount(async () => {
const res = await grafbase.request<Query>(TodosDocument)
if (res.todoCollection?.edges) {
res.todoCollection.edges.map((todo) => {
setTodos([...todos(), todo?.node as TodoType])
})
}
setMounted(true)
})

createEffect(async () => {
if (!mounted()) {
untrack(async () => {
if (todos().length > 0) {
const res = await grafbase.request<Mutation>(TodoUpdateDocument, {
id: "todo_01GYYYEEMR8EF1CC8F8AA0N29F",
title: "....",
})
}
})
}
})
ok this might work ok it does not
createEffect(async () => {
if (!mounted() && todos()) {
untrack(async () => {
if (todos().length > 0) {
console.log("run mutation")
const res = await grafbase.request<Mutation>(TodoUpdateDocument, {
id: "todo_01GYYYEEMR8EF1CC8F8AA0N29F",
title: "",
})
}
})
}
})
createEffect(async () => {
if (!mounted() && todos()) {
untrack(async () => {
if (todos().length > 0) {
console.log("run mutation")
const res = await grafbase.request<Mutation>(TodoUpdateDocument, {
id: "todo_01GYYYEEMR8EF1CC8F8AA0N29F",
title: "",
})
}
})
}
})
this still runs on load
nikivi
nikiviOP2y ago
this is close to what I want I think getting errors though
thetarnav
thetarnav2y ago
lol 39 replies I'm sure at least one of them is not by the author 🤣 So every time a todo is "marked" as a mutation it's uploaded to the db Do I understand the code correctly? that sounds very manual like you imperatively have to call "sync this" also what if you do
batch(() => {
setRunMutation(todo1)
setRunMutation(todo2)
})
batch(() => {
setRunMutation(todo1)
setRunMutation(todo2)
})
only the 2 will get synced
nikivi
nikiviOP2y ago
ok so i fixed the grafbase side of things the thing I want to fix is what you mention its kind of manual now
export const [GlobalContextProvider, useGlobalContext] = createContextProvider(
() => {
const [todos, setTodos] = createSignal<TodoType[]>([])
const [runMutateTodo, setRunMutateTodo] = createSignal("") // id of the todo to mutate

onMount(async () => {
// await createTodosForDev()
const res = await grafbase.request<Query>(TodosDocument)
if (res.todoCollection?.edges) {
res.todoCollection.edges.map((todo) => {
setTodos([...todos(), todo?.node as TodoType])
})
}
})
export const [GlobalContextProvider, useGlobalContext] = createContextProvider(
() => {
const [todos, setTodos] = createSignal<TodoType[]>([])
const [runMutateTodo, setRunMutateTodo] = createSignal("") // id of the todo to mutate

onMount(async () => {
// await createTodosForDev()
const res = await grafbase.request<Query>(TodosDocument)
if (res.todoCollection?.edges) {
res.todoCollection.edges.map((todo) => {
setTodos([...todos(), todo?.node as TodoType])
})
}
})
so this runs on loading of an app all todos get loaded into todos() signal
thetarnav
thetarnav2y ago
imo you should 1. split your state management into one that is synchronised with the db and one that is not 2. make a deferred effect in which you'll read all of your state and send it to the db 3. you can split it into multiple effects if that's better
nikivi
nikiviOP2y ago
my thinking was that I then just update todos() as i do and have an effect that if todos() get updated persist the changes to the database
createEffect(async () => {
if (runMutateTodo() !== "") {
untrack(async () => {
if (todos().length > 0) {
let todo = todos().find((todo) => todo.id === runMutateTodo())
console.log(todo?.priority, "priority")
console.log(runMutateTodo())
await grafbase.request<Mutation>(TodoUpdateDocument, {
id: runMutateTodo(),
todo: {
title: todo?.title,
done: todo?.done,
starred: todo?.starred,
priority: { set: todo?.priority },
note: todo?.note,
dueDate: todo?.dueDate,
},
})
setRunMutateTodo("")
}
})
}
})
createEffect(async () => {
if (runMutateTodo() !== "") {
untrack(async () => {
if (todos().length > 0) {
let todo = todos().find((todo) => todo.id === runMutateTodo())
console.log(todo?.priority, "priority")
console.log(runMutateTodo())
await grafbase.request<Mutation>(TodoUpdateDocument, {
id: runMutateTodo(),
todo: {
title: todo?.title,
done: todo?.done,
starred: todo?.starred,
priority: { set: todo?.priority },
note: todo?.note,
dueDate: todo?.dueDate,
},
})
setRunMutateTodo("")
}
})
}
})
like this this setRunMutateTodo i added because i had issues make it work without it
createEffect(async () => {
if (!global.editingTodo()) {
if (title() === "") {
global.setTodos(
global.todos().filter((todo) => todo.id !== props.todo.id)
)
return
}
let indexOfTodoToEdit = global
.todos()
.findIndex((todo) => todo.id === props.todo.id)
let newTodos = [...global.todos()]
newTodos[indexOfTodoToEdit].title = title()
newTodos[indexOfTodoToEdit].note = note()
newTodos[indexOfTodoToEdit].priority = priority()
newTodos[indexOfTodoToEdit].starred = starred()

if (showCalendar() && !dueDate()) {
newTodos[indexOfTodoToEdit].dueDate = todayDate()
} else {
newTodos[indexOfTodoToEdit].dueDate = dueDate()
}

global.setRunMutateTodo(props.todo.id)
global.setTodos(newTodos)
global.setTodoToEdit("")
}
})
createEffect(async () => {
if (!global.editingTodo()) {
if (title() === "") {
global.setTodos(
global.todos().filter((todo) => todo.id !== props.todo.id)
)
return
}
let indexOfTodoToEdit = global
.todos()
.findIndex((todo) => todo.id === props.todo.id)
let newTodos = [...global.todos()]
newTodos[indexOfTodoToEdit].title = title()
newTodos[indexOfTodoToEdit].note = note()
newTodos[indexOfTodoToEdit].priority = priority()
newTodos[indexOfTodoToEdit].starred = starred()

if (showCalendar() && !dueDate()) {
newTodos[indexOfTodoToEdit].dueDate = todayDate()
} else {
newTodos[indexOfTodoToEdit].dueDate = dueDate()
}

global.setRunMutateTodo(props.todo.id)
global.setTodos(newTodos)
global.setTodoToEdit("")
}
})
so on say TodoEdit
nikivi
nikiviOP2y ago
nikivi
nikiviOP2y ago
when you press return here that effect runs global.setRunMutateTodo(props.todo.id) this gets set and the effect runs and persists to db
thetarnav
thetarnav2y ago
damn I blinked and two pages of text appeared
nikivi
nikiviOP2y ago
hehe im excited actually if I get this to work its so sick right now it does actually work setRunMutateTodo with this though
thetarnav
thetarnav2y ago
you seem to always be lol how about this?
nikivi
nikiviOP2y ago
mm that sounds complex like in my case as it stands now only todos is actually persisted subtasks soon too
thetarnav
thetarnav2y ago
then make only todos get synced like this I just liked the separation synced from not synced
nikivi
nikiviOP2y ago
got time to show it? ❤️ I don't fully get what you mean doesn't have to be today
nikivi
nikiviOP2y ago
nikivi
nikiviOP2y ago
like this works and it's actually fairly clean i think actually with your approach
createEffect(async () => {
if (!global.editingTodo()) {
if (title() === "") {
global.setTodos(
global.todos().filter((todo) => todo.id !== props.todo.id)
)
return
}
let indexOfTodoToEdit = global
.todos()
.findIndex((todo) => todo.id === props.todo.id)
let newTodos = [...global.todos()]
newTodos[indexOfTodoToEdit].title = title()
newTodos[indexOfTodoToEdit].note = note()
newTodos[indexOfTodoToEdit].priority = priority()
newTodos[indexOfTodoToEdit].starred = starred()

if (showCalendar() && !dueDate()) {
newTodos[indexOfTodoToEdit].dueDate = todayDate()
} else {
newTodos[indexOfTodoToEdit].dueDate = dueDate()
}

global.setRunMutateTodo(props.todo.id)
createEffect(async () => {
if (!global.editingTodo()) {
if (title() === "") {
global.setTodos(
global.todos().filter((todo) => todo.id !== props.todo.id)
)
return
}
let indexOfTodoToEdit = global
.todos()
.findIndex((todo) => todo.id === props.todo.id)
let newTodos = [...global.todos()]
newTodos[indexOfTodoToEdit].title = title()
newTodos[indexOfTodoToEdit].note = note()
newTodos[indexOfTodoToEdit].priority = priority()
newTodos[indexOfTodoToEdit].starred = starred()

if (showCalendar() && !dueDate()) {
newTodos[indexOfTodoToEdit].dueDate = todayDate()
} else {
newTodos[indexOfTodoToEdit].dueDate = dueDate()
}

global.setRunMutateTodo(props.todo.id)
i won't have to do this in the end to trigger db mutation right? 1. split your state management into one that is synchronised with the db and one that is not 2. make a deferred effect in which you'll read all of your state and send it to the db 3. you can split it into multiple effects if that's better 1. i get i think 2. thats with createEffect?
thetarnav
thetarnav2y ago
just so I understand the requirements you only want to sync one todo at a time after it's been updated with that gql query where you target that todo by an id
nikivi
nikiviOP2y ago
I thought initially I would have to use createResource for this so the reqs are on load, load into todos() global signal the state of all todos user has thats done already now todos() is global signal just do what I do now mutate it etc but on every mutation, in background it sends an update to db so it persists I have this now but to do this after every mutation to todos() I have to do global.setRunMutateTodo(id_of_udpated_todo)
mutation TodoUpdate($id: ID!, $todo: TodoUpdateInput!) {
todoUpdate(by: { id: $id }, input: $todo) {
todo {
id
title
}
}
}
mutation TodoUpdate($id: ID!, $todo: TodoUpdateInput!) {
todoUpdate(by: { id: $id }, input: $todo) {
todo {
id
title
}
}
}
this is graphql mutation it accepts an id and the todo values
let todo = todos().find((todo) => todo.id === runMutateTodo())
await grafbase.request<Mutation>(TodoUpdateDocument, {
id: runMutateTodo(),
todo: {
title: todo?.title,
done: todo?.done,
starred: todo?.starred,
priority: { set: todo?.priority },
note: todo?.note,
dueDate: todo?.dueDate,
},
})
setRunMutateTodo("")
let todo = todos().find((todo) => todo.id === runMutateTodo())
await grafbase.request<Mutation>(TodoUpdateDocument, {
id: runMutateTodo(),
todo: {
title: todo?.title,
done: todo?.done,
starred: todo?.starred,
priority: { set: todo?.priority },
note: todo?.note,
dueDate: todo?.dueDate,
},
})
setRunMutateTodo("")
what I do now in affect that listens to setRunMutateTodo changes like in my mind ideally I wouldn't have to do this global.setRunMutateTodo(todo_id) but then I realize I need to know the updated id to do the mutation I can run a .find or something though or I dont know not sure if Im clear with explaining 🙂 you can clone repo and test it out its pretty nifty
thetarnav
thetarnav2y ago
this is why people need to learn mapArray
nikivi
nikiviOP2y ago
.map() you mean? i love .map() 😄
thetarnav
thetarnav2y ago
nah solid has a way more powerful one actually keyArray not map but the same pattern or maybe not 🤔
nikivi
nikiviOP2y ago
SolidJS
Solid is a purely reactive library. It was designed from the ground up with a reactive core. It's influenced by reactive principles developed by previous libraries.
nikivi
nikiviOP2y ago
fancy but I don't see how that would be used
thetarnav
thetarnav2y ago
oh no
nikivi
nikiviOP2y ago
oh yes, dm'd to setup setup with you 🙂
nikivi
nikiviOP2y ago
GitHub
Sync and refactor todos store (#3) · nikitavoloboev/kuskus@730b95d
* Sync and refactor todos store - moved todos into a separate store - changed signal into a resource - moved mutations to the store - setup an effect syncing the state to the db (unfinished) -...
thetarnav
thetarnav2y ago
@Jutanium btw this is a good example of using mapArray (keyArray actually but the concept is the same) to do something different than rendereing or deriving data it reconciles the input signal, to let you sync the state on the client with the db through graphql calls you have 3 different requests: 1. adding 2. updating 3. removing which correspond to keyArray lifecycle: 1. mapFn is called for new item 2. effect subscribes to item rerun 3. onCleanup called
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
Want results from more Discord servers?
Add your server