Skip to content

Instantly share code, notes, and snippets.

@egargan
Last active April 1, 2024 11:06
Show Gist options
  • Save egargan/20267f9d481e9493a9627a8448034b09 to your computer and use it in GitHub Desktop.
Save egargan/20267f9d481e9493a9627a8448034b09 to your computer and use it in GitHub Desktop.
A handful of files to help Svelte and Redux work together smoothly
<!--
Adds the given store to context, making it available to all components beneath this one in the
hierarchy.
@component
-->
<script lang="ts">
import { setContext } from 'svelte';
import type { Store } from '@reduxjs/toolkit';
import { STORE_KEY } from './constants';
export let store: Store;
setContext(STORE_KEY, store);
</script>
<slot />
import type { StoreEnhancerStoreCreator } from '@reduxjs/toolkit';
/**
* Redux store enhancer, creating a store that's compatible with Svelte's store contract.
*
* See https://redux.js.org/understanding/thinking-in-redux/glossary#store-enhancer.
*
* @example
* ```
* const todos = createSlice({
* name: 'todos',
* initialState: { todos: [] },
* reducers: {
* // ...
* },
* })
*
* const store = configureStore({ reducer: todos.reducer, enhancers: [ svelteStoreEnhancer ] });
* ```
*/
export default function svelteStoreEnhancer(
createStoreApi: StoreEnhancerStoreCreator
): StoreEnhancerStoreCreator {
return function (reducer, initialState) {
const reduxStore = createStoreApi(reducer, initialState);
return {
...reduxStore,
subscribe(fn: (value: any) => void) {
fn(reduxStore.getState());
return reduxStore.subscribe(() => {
fn(reduxStore.getState());
});
},
};
};
}
import { getContext } from 'svelte';
import type { Dispatch, Store } from '@reduxjs/toolkit';
import { STORE_KEY } from './constants';
/**
* Returns a dispatcher function for the contextual Redux store, used to dispatch actions.
*
* Assumes a Redux store is available in context via the `StoreProvider` component, or using the
* `STORE_KEY` directly.
*/
export function useDispatch<T = unknown>(): Dispatch {
const store: Store<T> = getContext(STORE_KEY);
return store.dispatch;
}
import { getContext } from 'svelte';
import { derived, type Readable } from 'svelte/store';
import type { Selector } from '@reduxjs/toolkit';
import { STORE_KEY } from './constants';
/**
* Returns a store whose value is the selected value from the contextual Redux store.
*
* @param selector Selector function for retrieving a value from the store
* @param equalityFn Boolean function that determines if the selector value has changed, ensuring
* the store is only updated when the value has changed.
*
* @example
* ```
* const thisTodo = useSelector((store) => store.todos[todoId]);
* ...
* <p>{$thisTodo.name}</p>
* ```
*/
export default function useSelector<T, S>(
selector: Selector<T, S>,
equalityFn?: (lhs: S, rhs: S) => boolean
): Readable<S> {
if (!equalityFn) {
equalityFn = (lhs: S, rhs: S) => lhs === rhs;
}
const store: Readable<T> = getContext(STORE_KEY);
let lastSelectorValue: S;
return derived(store, ($state: T, set) => {
const selectorValue: S = selector($state);
if (!equalityFn!(selectorValue, lastSelectorValue)) {
lastSelectorValue = selectorValue;
set(lastSelectorValue);
}
});
}
import type { Store } from '@reduxjs/toolkit';
import { getContext } from 'svelte';
import { STORE_KEY } from './constants';
/**
* Returns the Redux store that's available in context.
*
* Assumes a Redux store is available in context via the `StoreProvider` component, or using the
* `STORE_KEY` directly.
*/
export default function useStore<T>(): Store<T> {
const store: Store<T> = getContext(STORE_KEY);
return store;
}
@IPWright83
Copy link

Hey there, thanks for sharing this! I'm trying it out and I've a question, and a suggestion.

Firstly there's no constants file in here, which might be worth adding. Secondly I'm finding there's a race within the useSelector. When I'm trying this in a very simple application (being a Svelte n00bie) with the following:

<script>
import { useSelector } from "../store/useSelector";
import { useDispatch } from "../store/useDispatch";
import { numActions } from "../store/createStore";
import StoreProvider from "../StoreProvider.svelte";

export let label = "test";

let count = useSelector((store) => store.count);
const dispatch = useDispatch();

function onClick(event) {
    dispatch(numActions.add());
}
</script>

<StoreProvider>
    <button type="button" class="btn" on:click={onClick}>
        {label}
    </button>
    {count}
</StoreProvider>

<style>
.btn {
    border: thin solid red;
}
</style>

I'm getting an error Error: derived() expects stores as input, got a falsy value. It looks as though on the first run through (I guess before first render) the StoreProvider hasn't yet ran or been mounted (whatever the appropriate Svelte terminology is) and therefore the store hasn't been added to the context yet. Therefore the call to the getContext is returning undefined which derived is not happy about.

Do you have any suggestions on how to fix that?

@egargan
Copy link
Author

egargan commented Sep 21, 2023

Hey @IPWright83, thanks for your comment, it's great to hear that you're trying this out!

I haven't sat down and ran your example yet, but just at a glance, you're right when you say that useSelector is failing because the store isn't in context! Because you only have a single component here, you just need yo create your store, then you can just add the store to context yourself, with Svelte's setContext function -- as long as you do this above your useSelector call, things should work.

Alternatively, if you want to use StoreProvider, create a new component that creates a new store and passes it as a prop to a StoreProvider, then inside of this StoreProvider, render this component that you've shared here. This way, a store will be in context when this component runs.

About the constants file, I think it's easy enough to imagine what it looks like without adding it to the Gist, but thanks for the suggestion!

@IPWright83
Copy link

Thanks @egargan, I made some tweaks like you suggested and got it all working eventually. I'm hoping this will allow me to port my chart library from React to Svelte at some point as finding out how to share the state management is a big time saver.

@IPWright83
Copy link

I've been trying to look back at this today, trying to learn Svelte a bit more deeply... However going back over it I really can't seem to get it working. I know Svelte have been making some big changes, is there a chance this no longer works?

I've got a Svelte repl that I've been using. It seems as though the callback provided to derived in the useSelector simply never fires. Just wondering if you could take a quick 👀 at it @egargan and tell me if I'm doing something wrong? Thanks!

@egargan
Copy link
Author

egargan commented Mar 31, 2024

Hey @IPWright83! Bit of a sneaky issue this one, you're missing a $ before your use of width in your XAxis.svelte file, it should look like this!

<g>
  <text>{$width}</text>
  <g class="axis" style="user-select: none" transform={transform} bind:this={axis} />
</g>

For the following code...

const width = useSelector((store) => { 
  console.log("This never runs....");
  return store.width;
});

while you've successfully created a derived store called width from the main store, because you haven't 'subscribed' to this store anywhere (i.e. you haven't put $width anywhere), Svelte deliberately doesn't do anything with it.

As the docs touch on in the bit about stores, Svelte keeps track of how many subscriptions stores have. For derived stores, Svelte uses this information to ignore the work we're asking it to do in the useSelector hook. Which makes sense: why spend time running that callback that derives some value from the main store, if there's nothing subscribed to our derived store?

As soon as we add {$width} to your XAxis.svelte file, we're telling Svelte that we want to use the value from this derived store (we're creating an 'auto-subscription'), and so it'll do this work for us.

@IPWright83
Copy link

Thanks very much @egargan, I think I actually discovered this myself too reading through all the store docs in frustration, but appreciate you clarifying.

I think I'd missed the subtlety between a $ label in Svelte and the $ subscription which is annoying.

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