Use external data in XState FSM - reactjs

I'm trying to shim XState into an existing state management system (in a React app) and I'm trying to figure out how to represent the state that is already captured in the legacy state management without duplication.
import {useLegacyState} from 'legacy-state-system'
import {useMachine} from '#xstate/react'
import {MyMachine} from '../machine'
const MyComponent = () => {
const [data, setData] = useLegacyState();
const [state, send] = useMachine(MyMachine)
.....JSX etc....
}
For some of the data there is no overlap, but in at least one case (selecting an item on screen, causes the app to send({type: "SELECT_ITEM", itemId: "xyz"}) and fire setData("XYZ")), both legacy and new systems care about the item. XState is being used for UI State Management but the legacy system has side effects that depends on its internal state, so I can't only have data in XState.
My understanding of XState is that I should represent itemId as continuous data in XState's context, but that duplicates the data and I'm concerned that presents a maintenance issue since all developers forever will need to know to update both simultaneously. Is there a way for XState Context to take a value from a runtime-evaluated function? I know that there's assign if I want to push values into Context but that's susceptible to the same maintenance issue so I'm looking for a way to pull values from legacy-state-manager when I call state.context.itemId.

What about wrapping useMachine and using that instead?
import { useMachine as useXStateMachine } from '#xstate/react'
export const useMachine = (machine, options) => {
const [data, setData] = useLegacyState();
const [state, send] = useXStateMachine(machine)
const context = new Proxy({}, {
get: (_, prop) => {
try {
return state.context[prop] || data[prop]
} catch (_) {
return data[prop]
}
}
})
return [{...state, context}, send]
}

The view or the react layer gets updated every time the data store changes and renders it. Typically in a MVC architecture, these logics are built into the controller, where multiple data stores are combined and the resulting data is returned to the UI. In a hook based approach like how you have used, you an create services, that wraps the datastore logics, within it and return only the data required at the UI level.
import {useCustomService} from './services';
const MyComponent = () => {
const [uiData, updateUI] = useCustomService();
}

Related

Is there a downside to using a custom hook as a store instead of tools like redux or useReducer?

Just like the title, I am wondering if I could use a simple hook to save data which can be used by any consumer
export default () => ({data: 'some random data'})
And any consumer can use it like so
const { data } = useSomeDataStore()
If we want to make the data updatable, I can add a setter to it like so:
export default () => {
const [data, setData] = useState('some random data')
return {
data,
setData
}
}
So I am wondering, why do we need ContextAPI, redux and a thousand other state management tools? I understand that for complex and data intensive app, we might need some of these tools to manage it, but for a very simple app design, what are the downsides of this implementation?

Saving and restoring the full state of a React app

I'm building a VSCode extension using React. When the tab loses focus, VSCode shuts down the WebView and only reloads it when the tab gets focus again. The app fully reloads from the start.
VSCode already provides a way to save arbitrary state object and then get it back when restoring the WebView.
What remains is to serialize the full state of the React app (the whole React DOM, states etc) into a simple JSON-like object.
How can I serialize the full state of the React app and then reload it?
I know that React has some features like Server-Side Rendering - maybe they can be used to serialize DOM and state?
To accomplish that, you need some kind of global state object, which holds all the state data that you want to preserve. You can then save and restore this object using the VSCode API you mentioned.
There are several ways to do that and different 3rd-party libraries for this purpose. Here I will outline some of the options.
Context API
Context API is built into React. You need to create an instance and wrap your app with a context provider. Then you can access the state in your child components with useContext.
Here's an example of how you would use it to store some user and page data, as well as control some textarea field in a child component, which would normally be a local state.
const App = () => {
const [user, setUser] = useState();
const [textAreaValue, setTextAreaValue] = useState("");
const [currentPage, setCurrentPage] = useState("home");
// etc.
// this is your global state object that you can then save using VSCode magic
const globalState = { user, setUser, /* etc. */ };
return (
<GlobalStateContext.Provider value={globalState}>
<Child />
</GlobalStateContext.Provider>
);
}
...
const Child = () => {
const { textAreaValue, setTextAreaValue } = useContext(GlobalStateContext);
const handleChange = (e) => {
setTextAreaValue(e.target.value);
}
return (
<textarea value={textAreaValue} onChange={handleChange} />
);
}
Of course, this will be cumbersome if you have a lot of state data to manage. Furthermore, whenever any field in the context changes, all components using it will re-render. This could cause performance issues, so this solution does not scale well. It should be fine for a simple application though.
Custom store hook
Another solution would be to use a global store functionality. You could write a custom hook for that and then use it like this:
const Child = () => {
const { textAreaValue, setTextAreaValue } = useStore("textarea");
const handleChange = (e) => {
setTextAreaValue(e.target.value);
}
return (
<textarea value={textAreaValue} onChange={handleChange} />
);
}
I won't provide a full example of how to implement this for brevity, but here is one guide that could be useful.
3rd-party library
There are also 3rd-party libraries that implement the global store functionality. A popular choice is Redux, although I personally wouldn't recommend it if you haven't used it before, due to its verbosity and somewhat of a learning curve. Other options include Recoil, react-hooks-global-state and ReactN.

Access data already fetched with react query in other component

I'm new with React Query, and I have a question, I've being looking on the documentation but I can't find how should I access the data that is already fetched with useQuery() from another component.
I'm fetching const query = useQuery('todos', fetchFunction) from HomeComponent and I want to access that data that is already fetched in the TodosComponent. Is ok to fetch the data again with const query = useQuery('todos', fetchFunction) or is there anything like redux that the data is in something like Store so I can access it from any place ?
;)
It is definitely best to just call useQuery again, because it's the only thing that creates a subscription, so your component will re-render correctly if new data comes in. You can do it imperatively with queryClient.getQueryData('todos'), but it doesn't create a subscription.
Note that useQuery will not always trigger a fetch of the data. You can customize staleTime to tell react-query how long a resource is considered fresh, and as long as it's fresh, data will come from the internal cache only. If you set staleTime: Infinity, there will only be one fetch, and all other invocations will only read from the cache (apart from manual invalidations and garbage collection).
It's also best to extract useQuery calls to a custom hook:
const useTodos = () => useQuery('todos', fetchFunction)
HomeComponent:
const { data } = useTodos()
TodosComponent:
const { data } = useTodos()
You can create custom hook with useQueryClient
import { useQueryClient } from "react-query";
export const useGetFetchQuery = (name) => {
const queryClient = useQueryClient();
return queryClient.getQueryData(name);
};
And in component just write like this
const data = useGetFetchQuery("todos");
You can use options { refetchOnMount: false } to use it in many other components, it will render only once and make only one api call.

React redux sharing whole pages dependency injection

My team develops a few different React applications that share similar components and pages.
how would approach sharing whole pages within different React applications? given that these pages have large amount of components in different hierarchies and each application has its own implementation of accessing the backend?
we are using React, React hooks, Redux, Saga.
In the end those pages are React components, so best way would be to identify all common components in the different React applications your team is developing, and create a component library to put all those components so the are all in a single code base, and is easier to maintain and use. I recommend you check this package https://www.npmjs.com/package/create-react-library
I have used before, is really easy to get it run. Still you can create a new library on your own, but this library will save you some time.
Another approach would be a step forward using micro-frontends architecture, I haven't used yet but looks really promising.
You can check this link https://micro-frontends.org/ and are many resources out there to learn about it. Like I said, I haven't used yet so I can't make you any recommendations about it.
If you decide to use a library for common components, then you can pass actions and hooks you need to execute in the library as props to the components in the library
export const RegisterView = ({ login, useMutation, useQuery, ...props }) => {
const [username, setUsername] = useState('')
const [email, setEmail] = useState('')
const [phone, setPhone] = useState('')
const [submitError, setSubmitError] = useState(null)
const [register, { loading }] = useMutation(REGISTER)
const { data: duplicatedUsername } = useQuery(CHECK_IF_USERNAME_EXIST, {
skip: !username,
variables: { username }
})
const { data: duplicatedEmail } = useQuery(CHECK_IF_EMAIL_EXIST, {
skip: !email,
variables: { email }
})
const { data: duplicatedPhone } = useQuery(CHECK_IF_PHONE_EXIST, {
skip: !phone,
variables: { phone }
})
the component is too large, this is only a fragment, but hope this shows the case you will probably need to use. In this case useQuery, useMutation are hooks from apollo-client I'm passing from the app, the schemas for this calls are also in the library. The login prop is also a hook that calls the right login request, so the RegisterView component only receive those props coming from the app where is being imported and executes them.
Whatever logic your common component need to use or execute that is not shared between all apps, you must put it outside the library and pass it as props.
The create-react-library package also comes with a react app inside to test the components in your library so you are able to debug them and test them in isolation on the apps where you will import them.

React - Hooks + Context - Is this a good way to do global state management?

I am trying to find a good, clean, with little boilerplate, way to handle React's global state
The idea here is to have a HOC, taking advantage of React's new Hooks & Context APIs, that returns a Context provider with the value bound to its state. I use rxjs for triggering a state update on store change.
I also export a few more objects from my store (notably : the raw rxjs subject object and a Proxy of the store that always returns the latest value).
This works. When I change something in my global store, I get updates anywhere in the app (be it a React component, or outside React). However, to achieve this, the HOC component re-renders.
Is this a no-op ?
The piece of code / logic I think could be problematic is the HOC component:
const Provider = ({ children }) => {
const [store, setStore] = useState(GlobalStore.value)
useEffect(() => {
GlobalStore.subscribe(setStore)
}, [])
return <Context.Provider value={store}>{children}</Context.Provider>
}
GlobalStore is a rxjs BehaviorSubject. Every time the subject is updated, the state of the Provider component gets updated which triggers a re-render.
Full demo is available there: https://codesandbox.io/s/qzkqrm698q
The real question is: isn't that a poor way of doing global state management ? I feel it might be because I basically re-render everything on state update...
EDIT: I think I have written a more performant version that's not as lightweight (depends on MobX), but I think it generates a lot less overhead (demo at: https://codesandbox.io/s/7oxko37rq) - Now what would be cool would be to have the same end result, but dropping MobX - The question makes no sense anymore
I understand your need to handle a global state. I already found myself in the same situation. We have adopted similar solutions, but in my case, I've decided to completelly drop from ContextAPI.
The ContextAPI really sucks to me. It seems to pretend to be a controller based pattern, but you end up wrapping the code inside an non-sense HOC. Maybe I've missed he point here, but in my opinion the ContextAPI is just a complicated way to offer scoped based data flow.
So, I decided to implement my own global state manager, using React Hooks and RxJS. Mainly because I do not use to work on really huge projects (where Redux would fit perfectly).
My solution is very simple. So lets read some codes because they say more than words:
1. Store
I've created an class only to dar nome aos bois (it's a popular brazilian expression, google it 😊) and to have a easy way to use partial update on BehaviorSubject value:
import { BehaviorSubject } from "rxjs";
export default class Store<T extends Object> extends BehaviorSubject<T> {
update(value: Partial<T>) {
this.next({ ...this.value, ...value });
}
}
2. createSharedStore
An function to instantiate the Store class (yes it is just because I don't like to type new ¯\(ツ)/¯):
import Store from "./store";
export default function <T>(initialValue: T) {
return new Store<T>(initialValue);
}
3. useSharedStore
I created an hook to easily use an local state connected with the Store:
import Store from "./store";
import { useCallback, useEffect, useState } from "react";
import { skip } from "rxjs/operators";
import createSharedStore from "./createSharedStore";
const globalStore = createSharedStore<any>({});
type SetPartialSharedStateAction<S> = (state: S) => S;
type SetSharedStateAction<S> = (
state: S | SetPartialSharedStateAction<S>
) => void;
export default function <T>(
store: Store<T> = globalStore
): [T, SetSharedStateAction<T>] {
const [state, setState] = useState(store.value);
useEffect(() => {
const subscription = store
.pipe(skip(1))
.subscribe((data) => setState(data));
return () => subscription.unsubscribe();
});
const setStateProxy = useCallback(
(state: T | SetPartialSharedStateAction<T>) => {
if (typeof state === "function") {
const partialUpdate: any = state;
store.next(partialUpdate(store.value));
} else {
store.next(state);
}
},
[store]
);
return [state, setStateProxy];
}
4. ExampleStore
Then I export individual stores for each feature that needs shared state:
import { createSharedStore } from "hooks/SharedState";
export default createSharedStore<Models.Example | undefined>(undefined);
5. ExampleComponent
Finally, this is how to use in the component (just like a regular React state):
import React from "react";
import { useSharedState } from "hooks/SharedState";
import ExampleStore from "stores/ExampleStore";
export default function () {
// ...
const [state, setState] = useSharedState(ExampleStore);
// ...
function handleChanges(event) {
setState(event.currentTarget.value);
}
return (
<>
<h1>{state.foo}</h1>
<input onChange={handleChange} />
</>
);
}
GlobalStore subject is redundant. RxJS observables and React context API both implement pub-sub pattern, there are no benefits in using them together this way. If GlobalStore.subscribe is supposed to be used in children to update the state, this will result in unnecessary tight coupling.
Updating glubal state with new object will result in re-rendering the entire component hierarchy. A common way to avoid performance issues in children is to pick necessary state parts and make them pure components to prevent unnecessary updates:
<Context.Consumer>
({ foo: { bar }, setState }) => <PureFoo bar={bar} setState={setState}/>
</Context.Provider>
PureFoo won't be re-rendered on state updates as long as bar and setState are the same.

Resources