I'm porting a project from AngularJs to Vue3. In Angular we have a service component, which creates und supplies objects to components. The objects are automatically reactive, if they are used in a binding in a component. If component A changes an object, the View of component B shows the change of the state immediately.
Is there a way to accomplish such a service in Vue3? The goal is, to maintain an array of objects, and every change to the array is reflected in the Views of the different components.
Differences: Vue VS AngularJS
Vue is a UI framework that doesn't specifically have such entities as services, it is up to a developer to implement them. Also in contrast to Angular, Vue doesn't provide dependency-injection (DI) features.
Composables as service in Vue
In Vue, reusable component code is supposed to be provided by composables or composable functions, their reactivity is implemented with composition API:
// Usage: service = useLocalService()
const useLocalService = () => {
const someState = reactive({});
const someAction = () => /* change state */;
return { someState, someAction }
};
provide / inject to share services in Vue (compare DI)
For global singleton services that are inherent in Angular, service state is defined in a module and evaluated once, so it's shared across the app:
// Usage: import * as service from '...';
export const someGlobalState = reactive({});
export const someGlobalAction = () => /* change state */;
Whether it's beneficial to wrap it in a composable function for consistency depends on a situation:
// Usage: service = useGlobalService()
const someGlobalState = reactive({});
const someGlobalAction = () => /* change state */;
const useGlobalService = () => {
return { someGlobalState, someGlobalAction }
};
In Angular 2 or later, DI containers allow to provide service instances per component hierarchy. In Vue, this is done through provide/inject:
// Usage in a parent: setupRootOrNestedService()
const setupRootOrNestedService = () => {
const someRootOrNestedState = reactive({});
const someRootOrNestedAction = () => /* change state */;
provide('rootOrNestedService', { someRootOrNestedState, someRootOrNestedState });
};
// Usage in a child: service = useRootOrNestedService()
const useRootOrNestedService = () => {
return inject('rootOrNestedService');
};
This allows to identify services by tokens at any level and benefit from using DI pattern.
Pinia
Pinia (a Vue store) library provides lightweight state management influenced by Flux pattern. It relies on composition API and allows for multiple stores. The result is similar to the above, with the addition of a convention over code, extended Vue devtools, proper TypeScript typing, and a plugin layer:
// Usage as local service: service = useLocalOrGlobalService()
// Usage as global service: export default useLocalOrGlobalService()
// And: import * as service from '...';
const useLocalOrGlobalService = () => {
return defineStore('localOrGlobalService' + random(), {
state: () => ({}),
actions: {
someAction() { /* change state */ }
}
})
};
Pinia doesn't restrict ways in which a store is instantiated and provided to components, so it can be combined with provide/inject if necessary.
Just for the records: This is, what I came up with, as simple sample code. First I wrote a little service:
import { reactive } from "vue";
export const objectService = {
objects: reactive([]), // reactive is important, otherwise it doesn't work
start: function(){
for (var i = 0; i < 10; i++)
((j)=>{
setTimeout(()=> // I use setTimeout as example for an async operation
objectService.objects.push({value: j}), 1000);
})(i);
}
};
The point here is, that the service manipulates the state in an async operation (in real live systems a rest call or access to indexedDb, etc).
Now there are two components running in two separate Vue apps. The reason, why they run in separate apps is out of the scope of the issue here. App1:
<template>
<!--App1.vue-->
<p>Here is App1</p>
<div v-for="obj,ix in objectService.objects" :key="ix">{{obj.value}}
<input v-model="obj.value" />
</div>
</template>
<script>
import { defineComponent } from 'vue';
import { objectService } from '../services/object-service.js';
const App1 = defineComponent({
setup() {
return { objectService };
},
})
export default App1;
</script>
App2:
<template>
<!--App2.vue-->
<p>Here is App2</p>
<div v-for="obj,ix in objectService.objects" :key="ix">{{obj.value}}
<input v-model="obj.value" />
</div>
</template>
<script>
import { defineComponent } from 'vue';
import { objectService } from '../services/object-service.js';
const App2 = defineComponent({
setup() {
// const os = objectService;
objectService.start();
return { objectService };
},
})
export default App2;
</script>
These 2 components are identical with one exception: App2 calls the start method of the service. The result is shown in both components. And each of the components can alter the state, which is immediately reflected in the other component.
The point of the whole solution is the use of reactive({}) in the service. I hope, this is useful for somebody.
Again thanks to Estus Flask & hc_dev to point me into the right direction.
Related
I have been told that we should be using mock data and not interact with our real database resources.
I also been told that I should "Mock firebase object and return fake results from it", but where do I mock the firebase object, is it my actual component or is it in my test file and how do I actually mock it?
here is the code that I have:
// just a hook that fetches current user data
export const useGetCurrentUsersProfile = () => {
const userInfoRef = doc(firestore, "users", auth.currentUser?.uid ?? "_STUB_");
return useFirestoreDocument(
["userInfo"],
userInfoRef,
{
subscribe: false,
},
{
select: (data) => {
const currentUsersProfile = data.data() as UserInfo;
return currentUsersProfile;
},
enabled: !!auth.currentUser?.uid,
}
);
};
My react component that uses that hook to fetch data:
import React from "react";
import { useGetCurrentUsersProfile } from "../app/utilityfunctions";
function Siuu() {
const { data: currentUser } = useGetCurrentUsersProfile();
return (
<div>
<div>{currentUser?.name}</div>
</div>
);
}
export default Siuu;
/** #jest-environment jsdom */
import { render, screen } from "#testing-library/react";
import Siuu from "./siuu";
test("renders component", () => {
render(<Siuu />);
const randomElement = screen.getByText(/Jack/i);
expect(randomElement).toBeInTheDocument();
});
Where and how do I mock it, if I just wanted to return this data from my hook:
{name:"jack", email:"jack#gmail.com"}
Mocking is done in the test code, not in the real app code.
What it means is: you create a fake version of a thing, so that the real thing doesn't get used when the test makes the app do stuff. Jest has tools that help you do this.
In your case, it seems like the thing you should mock is the doc and/or useFirestoreDocument functions. Your code sample doesn't say where those two things come from, and I don't know firestore, so I'll just assume that both are imported from a single "some-firestore-pkg" package like so: import { doc, useFirestoreDocument } from 'some-firestore-pkg'
The simplest way would be to create a one-off mock for this test specifically, but if this firestore stuff is used through the app and you want to write tests for the rest, you'll want to read up on the tools and patterns that Jest provides for creating reusable mocks. For this answer, I'll do it all with one-off mocks.
/*
STEP 1: Import some-firestore-pkg package so we can manipulate it during the
tests. This will be the real firestore package.
*/
import firestorePkg from 'some-firestore-pkg'
// ...skipping down to your test
test("renders component", () => {
/*
STEP 2: replace the real `doc` function with a fake version that returns a
hard-coded value.
We MUST replace it to prevent the real one from doing any real work,
although we may not need to make the fake version return anything. But,
let's do so, in order to miminize the chance that this test will stop
working when the hook is modified in the future to verify the results of
each operation.
*/
let docSpy = jest.spyOn(firestorePkg, 'doc').mockReturnValue(
'A_FAKE_USER_INFO_REF'
)
// now, when the real hook calls the doc(...) function, it will get that value
/*
STEP 3: replace the real useFirestoreDocument with a fake version that
returns the hard-coded fake data that you want the app to receive during the
test. You can probably guess what this will look like:
*/
let useDocSpy = jest.spyOn(firestorePkg, 'useFirestoreDocument').mockReturnValue(
{ data: { name: "jack", email: "jack#gmail.com" } }
)
/*
STEP 4: now that we've "arranged," let's "act" and then "assert"
*/
let app = render(<Siuu />)
let randomElement = app.getByText(/Jack/i)
expect(randomElement).toBeInTheDocument()
// STEP 5: completely remove our fake versions so other tests aren't impacted
docSpy.mockRestore()
useDocSpy.mockRestore()
})
I have a react app that uses ContextAPI to manage authentication and I am trying to implement a similar thing in Svelte. [Web Dev Simplified][1]
In Authenticate.js I have this:
import React, { useContext, useState, useEffect } from "react"
import { auth } from "../firebase"
const AuthCt = React.createContext()
export function Auth() {
return useContext(AuthCt)
}
export function AuthComp({ children }) {
const [currentUser, setCurrentUser] = useState()
const [loading, setLoading] = useState(true)
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password)
}
function logout() {
return auth.signOut()
}
useEffect(() => {
const unmount = auth.onAuthStateChanged(user => {
setCurrentUser(user)
setLoading(false)
})
return unmount
}, [])
const value = {
currentUser,
login,
signup
}
return (
<AuthCt.Provider value={value}>
{!loading && children}
</AuthCt.Provider>
)
}
This context is used in other Login.js component like this:
import { Auth } from "./Authenticate"
const Login = () => {
const { currentUser, login } = Auth()
And in App.js I have:
import { AuthComp } from "./Authenticate";
function App() {
return (
<AuthComp>
<div> All others go here </div>
</AuthComp>
);
}
How do I achieve this in Svelte, particularly the Authenticate context?
I haven't been able to do much in Svelte as I don't know how to proceed from here. So far I have AuthComp.svelte. I don't know if I am doing the right thing.
<script>
import { getContext, setContext } from 'svelte';
import { auth } from '../firebase';
import { writable } from 'svelte/store';
let Auth = getContext('AuthCt')
setContext('Auth', Auth)
let currentUser;
let loading = true;
const unmount = auth.onAuthStateChanged(user => {
currentUser = user;
loading = false
});
function login(email, password) {
return auth.signInWithEmailandPassWord(email,password)
}
function logout() {
return auth.signOut()
}
const value = { currentUser, login, signUp }
</script>
<slot value={value}></slot>
Migrating from React Context to Svelte
Context in Svelte and React may seem similar, but they are actually used differently. Because at the core, Svelte's context is much more limited. But that's ok. In fact, it actually will make your code simpler to write and understand.
In Svelte, you have more tools at your disposal for passing data round your app (and keeping it in sync) than just context. Each one does pretty much one thing (making everything predictable), and they do it well. Of these, you have:
Context
Stores
Props
As someone who's recently switched from React to Svelte, I think I can help explain some of the differences between each of these and help you avoid some of my conceptual mistakes. I'll also go over some differences in life cycle methods, because if you used to use useEffect, you might feel very lost since Svelte doesn't have an equivalent API. Yet combining everything together in Svelte will make everything simple.
Context
Context in Svelte does one thing: pass data from a parent component to any children (not necessarily direct children). Unlike in React, context is not reactive. It is set once when the component mounts, and then will not be updated again. We'll get to "reactive context" in a second.
<!-- parent.svelte -->
<script>
import { setContext } from 'svelte'
setContext('myContext', true)
</script>
<!-- child.svelte -->
<script>
import { getContext } from 'svelte'
const myContext = getContext('myContext')
</script>
Notice that context involves two things, a key and a value. Context is set to a specific key, then the value can be retrieved using that key. Unlike React, you do not need to export functions to retrieve the context. Both the key and value for the context can be anything. If you can save it to a variable, you can set it to context. You can even use an object as a key!
Stores
If you have data that needs to stay in sync in multiple places across your app, stores are the way to go. Stores are reactive, meaning they can be updated after they're created. Unlike context in either React or Svelte, stores don't simply pass data to their children. Any part of your app can create a store, and any part of your app can read the store. You can even create stores outside of Svelte components in separate JavaScript files.
// mystore.ts
import { writable } from 'svelte/store'
// 0 is the initial value
const writableStore = writable(0)
// set the new value to 1
writableStore.set(1)
// use `update` to set a new value based on the previous value
writableStore.update((oldValue) => oldValue + 1)
export { writableStore }
Then inside a component, you can subscribe to the store.
<script>
import { writableStore } from './mystore'
</script>
{$writableStore}
The dollar sign subscribes to the store. Now, whenever the store is updated, the component will rerender automatically.
Using stores with context
Now that we have stores and context, we can create "reactive context"(a term I just made up, but it works). Stores are great because they're reactive, and context is great to pass data down to the children components. But we can actually pass a store down through context. This makes the context reactive and the store scoped.
<!-- parent.svelte -->
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
const writableStore = writable(0)
setContext('myContext', writableStore)
</script>
<!-- child.svelte -->
<script>
import { getContext } from 'svelte'
const myContext = getContext('myContext')
</script>
{$myContext}
Now, whenever the store updates in the parent, the child will also update. Stores can of course do much more than this, but if you were looking to replicate React context, this is the closest you can get in Svelte. It's also a lot less boilerplate!
Using "reactive context" with "useEffect"
Svelte does not have an equivalent of useEffect. Instead, Svelte has reactive statements. There's a lot on these in the docs/tutorial, so I'll keep this brief.
// doubled will always be twice of single. If single updates, doubled will run again.
$: doubled = single * 2
// equivalent to this
let single = 0
const [doubled, setDoubled] = useState(single * 2)
useEffect(() => {
setDoubled(single * 2)
}, [single])
Svelte is smart enough to figure out the dependencies and only run each reactive statement as needed. And if you create a dependency cycle, the compiler will yell at you.
This means that you can use reactive statements to update stores (and hence update the context). Here, the valueStore will be update on every keystroke to the input. Since this store is passed down through context, any child can then get the current value of the input.
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
// this value is bound to the input's value. When the user types, this variable will always update
let value
const valueStore = writable(value)
setContext('inputContext', valueStore)
$: valueStore.set(value)
</script>
<input type='text' bind:value />
Props
For the most part, props function exactly the same in React and Svelte. There are a few differences because Svelte props can take advantage of two-way binding (not necessary, but possible). That's really a different conversation though, and the tutorial is really good at teaching two-way binding with props.
Authentication in Svelte
Ok, now after all of that, let's look at how you'd create an authentication wrapper component.
Create an auth store
Pass the auth store down via context
Use Firebase's onAuthStateChanged to listen to changes in auth state
Subscribe to the auth store in the child
Unsubscribe from onAuthStateChanged when the parent is destroyed to prevent memory leaks
<!-- parent.svelte -->
<script>
import { writable } from 'svelte/store'
import { onDestroy, setContext } from 'svelte'
import { auth } from '../firebase'
const userStore = writable(null)
const firebaseUnsubscribe = auth.onAuthStateChanged((user) => {
userStore.set(user)
})
const login = (email, password) => auth.signInWithEmailandPassWord(email,password)
const logout = () => auth.signOut()
setContext('authContext', { user: userStore, login, logout })
onDestroy(() => firebaseUnsubscribe())
</script>
<slot />
<!-- child.svelte -->
<script>
import { getContext } from 'svelte'
const { login, logout, user } = getContext('authContext')
</script>
{$user?.displayName}
In Svelte, context is set with setContext(key, value) in a parent component, and children can access the value object with getContext(key). See the docs for more info.
In your case, the context would be used like this:
<script>
import { getContext, setContext } from 'svelte';
import { auth } from '../firebase';
import { writable } from 'svelte/store';
// you can initialize this to something else if you want
let currentUser = writable(null)
let loading = true
// maybe you're looking for `onMount` or `onDestroy`?
const unmount = auth.onAuthStateChanged(user => {
currentUser.set(user)
loading = false
});
function login(email, password) {
return auth.signInWithEmailandPassWord(email,password)
}
function logout() {
return auth.signOut()
}
const value = { currentUser, login, signUp }
setContext('Auth', value)
</script>
{#if !loading}
<slot></slot>
{/if}
Here, currentUser, login, and signup (not sure where that's coming from?) are set as context with setContext(). To use this context, you would probably have something like this:
<!-- App -->
<AuthComp>
<!-- Some content here -->
<Component />
</AuthComp>
<!-- Component.svelte -->
<script>
import { getContext } from 'svelte'
const { currentUser, login, signup } = getContext('Auth')
// you can subscribe to currentUser with $currentUser
</script>
<div>some content</div>
As written in the docs, context is not reactive, so currentUser is first converted into a store so it can be subscribed to in a child. As for the useEffect, Svelte has lifecycle functions that you can use to run code at different points, such as onMount or onDestroy.
If you're new to Svelte, their tutorial is very comprehensive with plenty of examples that you can refer back to.
Hope this helped!
I have a child class that returns some JSX (an ion-item) it uses a hook to control some ion-icons (using the hook like a state but only because I can use useEffect which is very handy).
I also have a Bluetooth class. This is home to all the important Bluetooth functions. The reason it is in its own class is because I need this code accessible everywhere in the app (including the list of devices (the ion-items mentioned above) it creates).
I do it like this:
const _Bluetooth = () => {
const [state, setState] = useState([]);
useEffect(() => {
addDevice();
}, [state]);
const devices: any[] = [];
const bluetoothInitialize = () => {
for (let i = 0; i < 5; i++) {
let a = {name: "test", mac: i.toString(), connected:false}
setState({found: [...state.found, a]});
}
}
const connect = (id) => {
console.log("connecting");
}
const addDevice = () => {
let d = state.found[state.found.length - 1]
devices.push(<BluetoothDeviceItem key={d.mac} mac={d.mac} name={d.name} onClick={(id) => connect(id)} connectingState={d.connected ? 'connected' : 'not_connected'}></BluetoothDeviceItem>);
}
return {
devices, bluetoothInitialize, connect
};
}
export default _Bluetooth;
I then create an instance of this class in another file which acts as a global file and then other files import this global file giving access to that one instance of the class:
import _Bluetooth from '../components/bluetooth/Bluetooth'
export const Bluetooth = _Bluetooth();
Unfortunately the _Bluetooth class doesn't work. Since I am using a hook, React expects the component to be rendered and therefore the component needs to return JSX. However I don't want it to return JSX but rather the accessible functions and variables.
Like I said above I am using these hooks more like states but only because of the useEffect function.
I could easily get this working by doing:
const state = {found: []}
and then directly pushing items to the array. This takes away my ability of using useEffect which makes my life a little bit easier but also cleans up the code a little bit.
Is it possible to use hooks without rendering the components / returning any JSX?
Question
Is it possible to implement a Custom React Hook that will instantiate a plain JS class only on the client in Nextjs?
The reason it can only be instantiated in the client is because the plain JS class uses sessionStorage, which is NOT available in Nextjs server.
It seems that a graceful implementation would be one of the following:
Instantiate it inside useEffect since that only runs on the client
Instantiate it inside a Custom Hook so the functionality can be shared across components.
Note:
I do not have a clear mental model of the steps React take with rehydration/render using SSR, so do correct me if my explanation is wrong.
References:
https://tech.willhaben.at/how-to-shoot-yourself-in-the-foot-in-react-401e7dbba720
https://joshwcomeau.com/react/the-perils-of-rehydration/
Classes
CheckoutStorage - plain JS class
Address - component using CheckoutStorage
Instantiate it inside a Custom Hook
// A simple class to retrive data from Session Storage.
class CheckoutStorage {
constructor() {
this._address = JSON.parse(sessionStorage.getItem('selectedAddress'))
// ...other class attributes...
}
get address() {
return this._address
}
// prefer function setters instead of having to do "checkoutStorage = something"
setAddress(address) {
sessionStorage.setItem('selectedAddress', address)
this._address = address
}
// ...other functions...
}
export default CheckoutStorage
Address (Function Component) using Custom Hook
Component that will be using CheckoutStorage
// Custom hook that will return an instantiated CheckoutStorage class on client.
const useCheckoutStorage = () => {
const [checkoutStorage, setCheckoutStorage] = useState(null)
useEffect(() => {
setCheckoutStorage(new CheckoutStorage())
}, [])
return checkoutStorage
}
const Address = () => {
// ..other code..
// Defined at the top of the component so other functions can use it.
const checkoutStorage = useCheckoutStorage()
const [addresses, setAddresses] = useState([])
const [selectedAddressId, setSelectedAddressId] = useState(-1)
useEffect(() => {
// Function that makes API call and calls `setAddresses(..)`
fetchAddresses()
// Get previously selected address ID in session storage
setSelectedAddressId(checkoutStorage.address.id)
}, [])
// other code...
return (
<div>...<div>
)
}
Error
Unable to find 'address' of undefined
This refers to the line:
setSelectedAddressId(checkoutStorage.address.id)
We are using checkoutStorage inside the useEffect()
At this point in time, useCheckoutStorage will return null as it's effect has not been executed.
It will only return the instantiated class CheckoutStorage after setting the state in fetchAdddresses()
Working Version of Address instantiating inside useEffect()
const Address = () => {
// ..other code..
// Previous try with a custom hook
// const checkoutStorage = useCheckoutStorage()
const [checkoutStorage, setCheckoutStorage] = useState(null)
const [addresses, setAddresses] = useState([])
const [selectedAddressId, setSelectedAddressId] = useState(-1)
useEffect(() => {
// Added the immediate 2 lines
const checkoutStorage = new CheckoutStorage()
setCheckoutStorage(checkoutStorage)
// Function that makes API call and calls `setAddresses(..)`
fetchAddresses()
// Get previously selected address ID in session storage
setSelectedAddressId(checkoutStorage.address.id)
}, [])
// other code...
return (
<div>...<div>
)
}
The above implementation works, however, if I would like to use checkoutStorage in another component, I would need to do something similar.
So revisiting the question:
Is it possible to implement a Custom React Hook that will instantiate a plain JS class only on the client in Nextjs?
If not, does this implementation makes sense?
Thanks in advance!
Stay Safe. Stay Home. Stay on StackFlow.
I'm using ReactJS, Redux (with server-side rendering) and react-router-redux as set up here and am getting a little thrown by how routes work with the rest of the redux state and actions.
For example, I have a members component with the route /members:
class Members extends Component {
static need = [
fetchMembers
]
render() {
...
the static need array specifies an action that populates an array on the state that is then mapped to the component props. That much works.
But then I have an individual member component with the route members/:memberId. How do I load that individual member in a way that works both client- and server-side.
What I'm doing now is the same:
class Member extends Component {
static need = [
fetchMembers
]
render() {
...
but then map just the single member
function mapStateToProps(state, ownProps) {
return {
member: state.member.members.find(member => member.id == ownProps.params.memberId),
};
}
This works but is obviously wrong. So the question is two-fold:
When the user clicks the router Link that has a query param (:memberId), how do I use that router param to query a specific document (assume a mongo database). Do I somehow trigger a separate action that populates an active member field on the redux state? Where does this happen, in the route component's componentDidMount?
How does this work with server-side rendering?
I’ve had the same question and seemed to find a way that works pretty well with my setup. I use Node, Express, React, React Router, Redux and Redux Thunk.
1) It really depends on where your data is. If the data needed for /member/:memberId is already in state (e.g. from an earlier call) you could theoretically filter through what you already have when componentDidMount is fired.
However, I'd prefer to keep things separate simply to avoid headaches. Starting to use one data source for multiple destinations/purposes throughout your app might give you long days down the road (e.g. when Component A needs more/less properties about the member than Component B or when Component A needs properties in a different format than Component B etc.).
This decision should of course be based on your use-case but due to the cost of API calls nowadays I wouldn't be afraid (at all) to make one when someone navigates to /member/:memberId.
2) I’ll answer with a simplified version of my typical setup:
Whenever a request comes through, I have this fella handle it.
// Imports and other jazz up here
app.use((req, res) => {
const store = configureStore({});
const routes = createRoutes(store);
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message);
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
const fetchedData = renderProps.components
.filter(component => component.fetchData)
.map(component => component.fetchData(store, renderProps.params));
Promise.all(fetchedData).then(() => {
const body = renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>
);
res.status(200).send(`<!doctype html>${renderToStaticMarkup(
<Html
body={body}
state={store.getState()}
/>)
}`);
});
} else {
res.status(404).send('Not found');
}
});
});
It’ll look for fetchData on the components that are about to be rendered, and make sure we have the data before we send anything to the client.
On each and every route, I have a Container. The Container’s sole purpose is to gather the data needed for that route. As you’ve touched upon this can happen server-side (fetchData in my case) or client-side (componentDidMount in my case). A typical Container of mine looks like this:
// Imports up here
class Container extends Component {
static fetchData(store, params) {
const categories = store.dispatch(getCategories());
return Promise.all([categories]);
}
componentDidMount() {
this.props.dispatch(getCategoriesIfNeeded());
}
render() {
return this.props.categories.length ? (
// Render categories
) : null;
}
}
Container.propTypes = {
categories: PropTypes.array.isRequired,
dispatch: PropTypes.func.isRequired,
params: PropTypes.object.isRequired,
};
function mapStateToProps(state) {
return {
categories: state.categories,
};
}
export default connect(mapStateToProps)(Container);
In the Container above I’m using getCategories and getCategoriesIfNeeded to make sure that I have the data needed for the route. getCategories is only called server-side, and getCategoriesIfNeeded is only called client-side.
Note that I have params available for both fetchData and componentDidMount (passed from connect()), which I could potentially use to extract something like :memberId.
The two functions used to fetch data above are listed below:
// Using this for structure of reducers etc.:
// https://github.com/erikras/ducks-modular-redux
//
// actionTypes object and reducer up here
export function getCategories() {
return (dispatch, getState) => {
dispatch({
type: actionTypes.GET_REQUEST,
});
return fetch('/api/categories').then(res => {
return !res.error ? dispatch({
error: null,
payload: res.body,
type: actionTypes.GET_COMPLETE,
}) : dispatch({
error: res.error,
payload: null,
type: actionTypes.GET_ERROR,
});
});
};
}
export function getCategoriesIfNeeded() {
return (dispatch, getState) => {
return getState().categories.length ? dispatch(getCategories()) : Promise.resolve();
};
}
As displayed above I have both dispatch and getState available thanks to Redux Thunk - that handles my promises too - which gives me freedom use the data I already have, request new data and do multiple updates of my reducer.
I hope this was enough to get you moving. If not don't hesitate to ask for further explanation :)
The answer, it turns out, was pretty simple. The implementation taken from Isomorphic Redux App ties the need static property on a component back to the router by passing the routes query params into the action creator.
So for the route:
items/:id
you'd use a component like
class Item extends Component {
static need = [
fetchItem
]
render() {
specifying that it needs the fetchItem action. That action is passed the route's query params, which you can use like
export function fetchItem({id}) {
let req = ...
return {
type: types.GET_ITEM,
promise: req
};
}
For a more detailed explanation about why this work, read marcfalk's answers, which describes a very similar approach.