Skip to content

Instantly share code, notes, and snippets.

@koenbok

koenbok/store.ts

Last active Nov 13, 2020
Embed
What would you like to do?
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();
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;
}
@jeromefarnum

This comment has been minimized.

Copy link

@jeromefarnum 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

This comment has been minimized.

Copy link
Owner Author

@koenbok koenbok commented Apr 2, 2019

Good point, thanks!

@koenbok

This comment has been minimized.

Copy link
Owner Author

@koenbok 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

This comment has been minimized.

Copy link
Owner Author

@koenbok 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

This comment has been minimized.

Copy link

@benjamindenboer benjamindenboer commented May 7, 2019

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

@arturogh

This comment has been minimized.

Copy link

@arturogh arturogh commented May 31, 2019

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

This comment has been minimized.

Copy link

@jordandobson jordandobson commented Jun 12, 2019

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

This comment has been minimized.

Copy link

@carlskov carlskov commented Sep 19, 2019

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

This comment has been minimized.

Copy link

@jeroenslor jeroenslor commented Oct 20, 2019

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

@samjt

This comment has been minimized.

Copy link

@samjt 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

This comment has been minimized.

Copy link

@maxsteenbergen maxsteenbergen commented Mar 4, 2020

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

@jacopocolo

This comment has been minimized.

Copy link

@jacopocolo jacopocolo commented Mar 16, 2020

@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

This comment has been minimized.

Copy link

@maxsteenbergen maxsteenbergen commented Mar 17, 2020

@jacopocolo Thanks! Massively helpful!

@rnz269

This comment has been minimized.

Copy link

@rnz269 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

This comment has been minimized.

Copy link

@rnz269 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

This comment has been minimized.

Copy link

@irlabs 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

This comment has been minimized.

Copy link

@thijsm 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

This comment has been minimized.

Copy link

@strinkaus-gd strinkaus-gd commented Sep 30, 2020

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

This comment has been minimized.

Copy link

@cereallarceny cereallarceny commented Oct 9, 2020

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

@jacopocolo

This comment has been minimized.

Copy link

@jacopocolo jacopocolo commented Oct 9, 2020

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

This comment has been minimized.

Copy link

@DvDriel 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

This comment has been minimized.

Copy link

@jacopocolo 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.