Update JSS sheet on specific redux state subtree change - reactjs

I am injecting JSS into a manually loaded iframe, and then launching a reload whenever certain values in my redux state change, as follows:
export default function ETFStyleUpdater({ sheet, id }: Props) {
const readerConfig = useAppSelector((state) => state.bookReader[id] || DEFAULT_BOOK_READER_CONFIG_STATE);
useEffect(() => {
sheet.update(readerConfig);
}, [readerConfig]);
return <></>;
}
The react code is loaded as follows:
const container = document.body.appendChild(document.createElement("div"));
createRoot(container!).render(
<Provider store={store}>
<ETFStyleUpdater sheet={sheet} id={bookId} />
</Provider>,
);
While this felt nasty even when I wrote it, it worked fine with webpack. I am now moving to vitejs, which complains with a
Uncaught Error: #vitejs/plugin-react can't detect preamble. Something is wrong.
This all gets loaded dynamically into an iframe but by accessing the store via a property on window.parent, it worked just fine with webpack.
How can I get the same effect without creating a new react root and doing a nasty hack with useEffect? I tried to put a reference to the sheet onto window and use a redux-toolkit extraReducer (or middleware) but it doesn't seem to update, even though there is a reference there.

redux-watch seems to do the trick.
let w = watch(store.getState, "bookReader");
store.subscribe(
w((newVal) => {
sheet.update(newVal[bookId]);
}),
);

Related

react-router-dom and redux subapp: useNavigator causes redux reload

I'm using react-router-dom v6. I'm versed with redux and react but only just starting to "go deep" with react-router-dom.
I have nested routes. One of the "nests" has access to the redux <Provider> context.
Despite navigating within the "nest" of Routes with access to the redux context, all of the rendered components in the Router context are being re-rendered - and with it resetting redux.
Finally, there are several reasons for this design, but one of them is so that I can initialize the middleware for a given project (specified in the url).
const middleware = (projectId) => {
let initialized = false;
return (store) => (next) => (action) => {
if (!initialized) {
initialized = true;
next({ type: "SET_READY", projectId });
}
next(action);
};
};
Here is a link to the sandbox that highlights the issue.
Thank you #DrewReese for engaging me on this issue.
In the sandboxed description of the issue, I solve the issue by memoizing the function that instantiates the store. The middleware has a two-step initialization process: (1) set the project id (2) load the store. The store value in the second step depends on what is returned by the server (i.e., if null, use a new store).
// core-app.jsx
// Three versions of the store instantiation process
// 1. resets store on every new page within the project detail view (bad)
// const storeWithProject = store(projectId);
// 2. ##INIT instantiates using initialState where projectId = null (see reducer)
// const storeWithProject = useMemo(() => store(projectId), [projectId]);
// 3. The comprehensive solution
const storeWithProject = useMemo(
() => store(projectId, seedState(projectId)),
[projectId]
);
This all said, in the actual app, I wasn't able to get use the redux <Provider> within the <BrowserRouter> context. I was not able to prevent a re-render of the SubApp component with every route change (as described above); my reliance on useEffect to fetch data (e.g., list of projects) at various points in the app is beyond my current skill-set :-/ (prop and state changes in context of react-router is tough-stuff to manage).
The solution that I'm working on now is to "raise" the redux provider to minimize the number of parents that might trigger a re-render. A somewhat disappointing conclusion because I can't leverage the "natural" entry point for when to instantiate the redux store.

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.

Etherjs "window" object not accessible in Next.js

I have a relatively basic project using solidity and react working as a single page dApp using Create React App. However I am trying to move this now to Nextjs and have hit a hurdle which I assume is something to do with the server side stuff Nextjs does. I have removed all the redundant code and just provide enough to generate the error:
import { ethers, Contract } from 'ethers';
import Project from '../src/artifacts/contracts/Project.sol/Project.json';
const contractAddress = process.env.contract_address;
export default function App() {
const provider = new ethers.providers.Web3Provider(window.ethereum);
console.log(provider.getSigner())
return (
<div className="App">
<p>Hello!</p>
</div>
);
}
This errors with:
window is not defined
I saw someone else suggest loading and setting it via state like so:
const [provider, setProvider] = useState({})
React.useEffect(() => {
setProvider(new ethers.providers.Web3Provider(window.ethereum))
}, []);
const signer = provider.getSigner();
But this returns: TypeError: provider.getSigner is not a function
However if i comment out this code, refresh and let the page load, then uncomment the code and let hot reload refresh the component I get no such error and can successfully console.log the signer.
Pulling my limited supply of hair out trying to resolve this, any help would be appreciated.
I have managed to get this working whilst sticking with my functional components.
Within useEffect I included a statement to check if the windowobject was undefined:
if (typeof window.ethereum !== "undefined" || (typeof window.web3 !== "undefined")) {
// Existing code goes here
}
And then had to make sure that any variables that I wanted to use outside of this if statement were saved to state as well as declared within the statement. Like:
const provider = new ethers.providers.Web3Provider(window.ethereum);
setProvider(provider)
This seemed to solve most of the issues with moving from CRA to Next and I now have the dApp back up and running.

React router provides javascript object instead of original type on url address bar reload

I am explining my problem with just the relevant code, as the full example is in this codesandbox link.
I am passing some props through a link to a component.
These props, have a firebase timestamp.
The props are passed correctly when the component is called through the link.
Link:
<Link to={{
pathname:path,
state: {
project
},
}} key={project.id}>
<ProjectSummary project={project} deleteCallback={projectDelete}/>
</Link>
Route:
<Route
path='/project/:id'
render={({ location }: {location: Location<{project: IFirebaseProject}>}) => {
const { state } = location;
const returnedComponent = state ? <ProjectDetails project={state.project} /> :
<ProjectDetails project={undefined}/>;
return returnedComponent;
}}
/>
and received by the ProjectList component, like this:
<div>{moment(stateProject.createdAt.toDate()).calendar()}</div>
My problem is that when the component is called through the link, props are passed and everything works fine, but, when I re-enter in the url adress bar, as the access to the component is not through the link, I would expect that the Route's render returned an undefined project (check route:
const returnedComponent = state ? <ProjectDetails project={state.project} /> : <ProjectDetails project={undefined}/>;) but, it returns the last passed project, with the timestamp as a plain Javascript object instead of a Timestamp type. So I get the error:
TypeError: stateProject.createdAt.toDate is not a function
Because the toDate() function is not available in the plain Javascript object returned, it is the Timestamp firebase type. Seems that for this specific case, the router is keeping it as a plain js object, instead of the original Timestamp instance. I would expect the route to return always the proyect undefined if not called from the link, as the props are not passed in (supposedly), but its not the case on the reload from the url address bar.
Curiously, in the codesandbox project, it does not reproduce, it fetches the data (you will be able to see the console.log('project fetched!!') when the project received is undefined).
However thrown from the dev server it happens. Might have something to do.
Find the git url if you wish to clone and check: https://github.com/LuisMerinoP/my-app.git
Remember that to reproduce you just need to enter to the link, and then put the focus in the explorer url address bar en press enter.
I case this might be the expected behaviour, maybe there is a more elegant way to way to deal with this specific case instead of checking the type returned on the reload. I wonder if it can be known if it is being called from the address bar instead of the link.
I know I can check the type in my component and fix this, creating a new timeStamp in the component from the js object returned, but I do not expect this behaviour from the router and would like to understand what is happenning.
Problem: Non-Serializable State
It returns the last passed project, with the timestamp as a plain Javascript object instead of a Timestamp type
I do not expect this behaviour from the router and would like to understand what is happening.
What's going on is that the state is being serialized and then deserialized, which means it's being converted to a JSON string representation and back. You will preserve any properties but the your methods.
The docs should probably be more explicit about this but you should not store anything that is not serializable. Under the hood React Router DOM uses the browser's History API and those docs make it more clear.
Suggestions
as in typescript is an assertion. It how you tell the compiler "use this type even though it's not really this type". When you have something that really is the type then do not use as. Instead apply a type to the variable: const project: IFirebaseProject = {
Your getProjectId function to get an id from a URL is not necessary because React Router can do this already! Use the useParams hook.
Don't duplicate props in state. You always want a "single source of truth".
Fetching Data
I played with your code a lot because at first I thought that you weren't loading the project at all when the page was accessed directly. I later realized that you were but by then I'd already rewritten everything!
Every URL on your site needs to be able to load on its own regardless of how it was accessed so you need some mechanism to load the appropriate project data from just an id. In order to minimize fetching you can store the projects in the state of the shared parent App, in a React context, or through a global state like Redux. Firestore has some built-in caching mechanisms that I am not too familiar with.
Since right now you are using dummy placeholder data, you want to build a way to access the data that you can later replace your real way. I am creating a hook useProject that takes the id and returns the project. Later on just replace that hook with a better one!
import { IFirebaseProject } from "../types";
import { projects } from "./sample-data";
/**
* hook to fetch a project by id
* might initially return undefined and then resolve to a project
* right now uses dummy data but can modify later
*/
const useProject_dummy = (id: string): IFirebaseProject | undefined => {
return projects.find((project) => project.id === id);
};
import { IFirebaseProject } from "../types";
import { useState, useEffect } from "react";
import db from "./db";
/**
* has the same signature so can be used interchangeably
*/
const useProject_firebase = (id: string): IFirebaseProject | undefined => {
const [project, setProject] = useState<IFirebaseProject | undefined>();
useEffect(() => {
// TODO: needs a cleanup function
const get = async () => {
try {
const doc = await db.collection("projects").doc(id).get();
const data = doc.data();
//is this this right type? Might need to manipulate the object
setProject(data as IFirebaseProject);
} catch (error) {
console.error(error);
}
};
get();
}, [id]);
return project;
};
You can separate the rendering of a single project page from the logic associated with getting a project from the URL.
const RenderProjectDetails = ({ project }: { project: IFirebaseProject }) => {
return (
<div className="container section project-details">
...
const ProjectDetailsScreen = () => {
// get the id from the URL
const { id } = useParams<{ id: string }>();
// get the project from the hook
const project = useProject(id ?? "");
if (project) {
return <RenderProjectDetails project={project} />;
} else {
return (
<div>
<p> Loading project... </p>
</div>
);
}
};
Code Sandbox Link

Component unmounts when parent state changes

I'm using React 16.8.2, and I'm having a problem with children of my component unmounting whenever state is changed in the app component.
Here's the scenario:
I have App.jsx (a functional component) with a number of state variables (useState)
The setters for some of these state variables are passed down the tree through a Context provider (useContext in the descendent)
I have a menu component (descendent of app), that invokes these setters to (for example) show a modal dialog
I have a modal dialog component (child of App), that uses the state variable as a property to determine whether it is open or not -- standard React stuff I think.
My problem: when any state variables in App are changed (through hooks of course), the children of App are unmounted and remounted- even if they have no connection to the state being changed. They aren't just re-rendered - the children are unmounted and their state is re-initialized. So the fields are cleared on my dialog, when they shouldn't be, for example.
This is already a fairly complex application, so I've spent a lot of time today isolating the problem. I then set up a simple create-react-app to try to replicate this behavior there - but this test app behaves like it should. Changing parent state, whether through a prop callback, or through a context-provided callback from the child - re-renders but does not unmount/remount and child state remains intact.
But in my real app, the components re-mount and child state gets re-initialized.
I've simplified it down to the barest that I can - I'm setting a fake state variable "foo" with "setFoo" through the Context from the child. Even though foo is not used by any component, changing the value of foo causes the children of App to unmount/remount.
In App.jsx:
const App = props => {
const [foo, setFoo] = useState(false);
// ...
const appControl = {
toggleFoo: () => setFoo(!foo);
};
// ...
return (
<AppContext.Provider value={appControl}>
... a bunch of stuff not using foo anywhere
... including, deep down:
<Menu />
</AppContext.Provider>
);
};
In Menu.jsx:
const Menu = props => {
const appControl = useContext(AppContext);
// ...
return (
... super simplified for test
<div onClick={appControl.toggleFoo}>
Toggle Foo
</div>
);
};
If I understand state properly, I do believe that changing state should result in children being re-rendered, but not re-mounted. This is what I'm seeing in my simple create-react-app test, but not in my real app.
I do see that I'm not on the latest React - perhaps upgrading will fix this?
Thanks for any insight on what I may be doing wrong, or misunderstanding here.
Solved. This is an interesting one. Here's what happened.
In my App component, I had a fairly deep tree of HOC's. Due to some dubious decisions on my part, I ended up breaking App into two components. App and AppCore. I had a reason for it, and it seemed to make sense at 3am. But to be both quick and dirty, I stuck AppCore as a const, inside my App function. I remember thinking to myself "I wonder what problems this will cause?" Now I know. Perhaps a React expert can fully explain this one to me though, as I don't see the difference between JSX assigned to a constant, and JSX returned directly. But there clearly is, and this is simple to reproduce.
To reproduce, create-react-app a test app:
create-react-app test
cd test
Then replace the contents of App.js with:
import React, { useState, useEffect } from "react";
const Menu = props => <div onClick={props.click}>Toggle Foo</div>;
const Test = props => {
useEffect(() => {
console.log("mounted");
return () => console.log("unmounted");
}, []);
return null;
};
const App = props => {
const [foo, setFoo] = useState(false);
// this is the root of the problem
// move this line outside of the function body
// and it mounts/unmounts correctly
const AppCore = props => <Test />;
return (
<>
<Menu click={() => setFoo(!foo)} />
<AppCore />
</>
);
};
export default App;
Then npm start, and when you click on "Toggle Foo" you'll see that the Test component is unmounted/remounted.
The solution here, is to simply move AppCore out of the function body. In my real app, this means I have some refactoring to do.
I wonder if this would be considered a React issue?

Resources