Skip to content

Instantly share code, notes, and snippets.

@koenbok
Last active November 10, 2023 00:17
Show Gist options
  • Save koenbok/ae7b94f9fefccc16a34589af344db789 to your computer and use it in GitHub Desktop.
Save koenbok/ae7b94f9fefccc16a34589af344db789 to your computer and use it in GitHub Desktop.
import * as React from "react";
/**
A hook to simply use state between components
Warning: this only works with function components (like any hook)
Usage:
// You can put this in an central file and import it too
const useStore = createStore({ count: 0 })
// And this is how you use it from any component
export function Example() {
const [store, setStore] = useStore()
const updateCount = () => setStore({ count: store.count + 1 })
return <div onClick={updateCount}>{store.count}</div>
}
*/
export function createStore<T>(state: T) {
// Store the initial state, copy the object if it's an object
let storeState: T = typeof state === "object" ? { ...state } : state
// Keep a list of all the listener, in the form of React hook setters
const storeSetters = new Set<Function>()
// Create a set function that updates all the listeners / setters
const setStoreState = (state: Partial<T>) => {
// If the state is an object, make sure we copy it
storeState =
typeof state === "object" ? { ...storeState, ...state } : state
// Update all the listeners / setters with the new value
storeSetters.forEach((setter) => setter(storeState))
}
// Create the actual hook based on everything above
function useStore(): [T, typeof setStoreState] {
// Create the hook we are going to use as a listener
const [state, setState] = React.useState(storeState)
// If we unmount the component using this hook, we need to remove the listener
React.useEffect(() => () => storeSetters.delete(setState), [])
// But right now, we need to add the listener
storeSetters.add(setState)
// Return the state and a function to update the central store
return [state, setStoreState]
}
return useStore
}
@jeromefarnum
Copy link

jeromefarnum commented Apr 1, 2019

not to be a pedant, but it looks like useMemo is an unneeded import.

Also, personally, this is very timely. Thanks for sharing 👍

@koenbok
Copy link
Author

koenbok commented Apr 2, 2019

Good point, thanks!

@koenbok
Copy link
Author

koenbok commented Apr 2, 2019

Fixed a small issue that would warn: Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().

@koenbok
Copy link
Author

koenbok commented May 4, 2019

This works for overrides too:

import * as React from "react"
import { Override } from "framer"
import { createStore } from "./Store"

const useStore = createStore({ rotate: 0 })

export function SetRotate(): Override {
    const [store, setStore] = useStore()
    return {
        onTap() {
            setStore({ rotate: store.rotate + 90 })
        },
    }
}

export function DoRotate(): Override {
    const [store, setStore] = useStore()
    return {
        animate: { rotate: store.rotate },
    }
}

@benjamindenboer
Copy link

@koenbok In the Override example it should be import { createStore } from "./store" (uncapitalized), right?

@arturogh
Copy link

For stores with many properties like:

{
title: 'ABC',
count: 1,
url: 'abc.com'
}

Am I right that you basically need to copy the whole object (all the properties) and then just overwrite the ones you want to update?

@jordandobson
Copy link

For stores with many properties like:

{
title: 'ABC',
count: 1,
url: 'abc.com'
}

Am I right that you basically need to copy the whole object (all the properties) and then just overwrite the ones you want to update?

Yes you would need to do that when you set it if you use an object. You can also use the existing value and add on your new properties like this...

setStore({...store, url: 'framer.com'})

There are probably a few other ways to do that too.

@koenbok - Does using the store re-run the overrides like the data object?

@carlskov
Copy link

This is very useful. One question from a React newbie. How can I make one sibling component rerender when the store gets changed by another sibling component?

@jeroenslor
Copy link

Just out of interests, why not just use context for this?

@samjt
Copy link

samjt commented Nov 25, 2019

React context changes cause all components in the context tree to re-render, which can cause slowdowns.
This is one reason that Redux tried and abandoned using context.

@maxsteenbergen
Copy link

@koenbok Could you expand on this example, especially on the bit of a "central file"?

@jacopocolo
Copy link

@maxsteenbergen I think I can help!

What you want is a a useStore.tsx file like this:

import { createStore } from "./store"

export const useStore = createStore({
//your store variables
})

And a store.ts file like this:

import * as React from "react"
import { useState, useEffect } from "react"

export function createStore<T>(state: T) {
    let storeState: T = Object.assign({}, state)
    const storeSetters = new Set()

    const setStoreState = (state: Partial<T>) => {
        storeState = Object.assign({}, storeState, state)
        storeSetters.forEach(setter => setter(storeState))
    }

    function useStore(): [T, typeof setStoreState] {
        const [state, setState] = useState(storeState)
        useEffect(() => () => storeSetters.delete(setState), [])
        storeSetters.add(setState)
        return [state, setStoreState]
    }

    return useStore
}

Then you can import both into a component or an override:

import { createStore } from "./store"
import { useStore } from "./useStore"

And initialize them as such: const [store, setStore] = useStore()

@maxsteenbergen
Copy link

maxsteenbergen commented Mar 17, 2020

@jacopocolo Thanks! Massively helpful!

@rnz269
Copy link

rnz269 commented Mar 17, 2020

I'm trying to update specific properties of a larger object store, but the below isn't working:

const moveHandler = (point, card_number) => { setStore(prevStore => { const newStore = { ...prevStore } newStore.last_position[card_number] = point.x return newStore }) }

Am I unable to access the previous store object as done above? This is how I set state in React, using the previous state object as an argument to setState, spreading this previous state object into the new state object, modifying the property within the new state object I'd like to change, and then returning the new state object.

@jacopocolo @koenbok

@rnz269
Copy link

rnz269 commented Mar 17, 2020

@jacopocolo @koenbok

Never mind, I figured it out. Simply create the new object and then return in setStore.

const moveHandler = (point, card_number) => { const newStore = { ...store } newStore.last_position[card_number] = point.x setStore(newStore) }

@irlabs
Copy link

irlabs commented Jul 28, 2020

If I use the store.tsx in Framer (2020.31), it gives an error with setter (line 23)

(parameter) setter: unknown
This expression is not callable.
  Type '{}' has no call signatures.(2349)

How can I fix this?

@thijsm
Copy link

thijsm commented Aug 19, 2020

If I use the store.tsx in Framer (2020.31), it gives an error with setter (line 23)

(parameter) setter: unknown
This expression is not callable.
  Type '{}' has no call signatures.(2349)

How can I fix this?

Try changing the second line in the function to:

const storeSetters = new Set<(state: T) => void>()

For an easy copy/paste:

import * as React from "react"
import { useState, useEffect } from "react"

/*
A hook to simply use state between components
Usage:
	
// You can put this in an central file and import it too
const useStore = createStore({ count: 0 })
	
// And this is how you use it from any component
export function Example() {

	const [store, setStore] = useStore()
	const updateCount = () => setStore({ count: store.count + 1 })
	
	return <div onClick={updateCount}>{store.count}</div>
}
*/

export function createStore<T>(state: T) {
    let storeState: T = Object.assign({}, state)
    const storeSetters = new Set<(state: T) => void>()

    const setStoreState = (state: Partial<T>) => {
        storeState = Object.assign({}, storeState, state)
        storeSetters.forEach((setter) => setter(storeState))
    }

    function useStore(): [T, typeof setStoreState] {
        const [state, setState] = useState(storeState)
        useEffect(() => () => storeSetters.delete(setState), [])
        storeSetters.add(setState)
        return [state, setStoreState]
    }

    return useStore
}

@strinkaus-gd
Copy link

Just tried this now, awesome work @koenbok. I'm using createStore with a navigation code component and an overridden Framer Page component to easily emulate the navigation in a mobile app with custom effects etc.

This is so useful, I'm wondering if you should promote it more or ship it as part of Framer 💯 😃

@cereallarceny
Copy link

cereallarceny commented Oct 9, 2020

@koenbok What's the difference between this and the Data() function already available in Framer?

@jacopocolo
Copy link

Not Koen but: as far as I understand the Data() function only works between overrides. This is (potentially) shared across all overrides and components in a project.

@DvDriel
Copy link

DvDriel commented Oct 20, 2020

@jacopocolo How would the code look like if you use this to share data between two different components that both have their own file? I do not understand where you initialise the store and the value you want to share.

For instance, I have these components:

  • Slider.tsx
  • Dial.tsx

I want to share a numeric value between the slider and the dial, where do I define the store and where do I initialise this value?

Many thanks in advance!

@jacopocolo
Copy link

jacopocolo commented Oct 20, 2020

@DvDriel first of all you'd want to set up your store and impot like this: https://gist.github.com/koenbok/ae7b94f9fefccc16a34589af344db789#gistcomment-3214773

Then in your components you'd add:
const [store, setStore] = useStore()

This allows you to read the store as a regular variable {store.var}. Or to write a variable with the setStore hook setStore({varName: value}).

The store can contain a single variable or a set of objects.

@koenvanham
Copy link

@koenbok Would this also work in Framer Web? I tried to insert the code but get an error:
image

@koenbok
Copy link
Author

koenbok commented Feb 6, 2021

It seems like TypeScript got a bit stricter with warnings, but it should totally work.

@koenvanham
Copy link

koenvanham commented Feb 8, 2021

Great, got it working indeed. What would be the right way to get this working on a class?

EDIT: nvm, figured out it was preferably to convert old class to a function. got it working now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment