ElectronJS - sharing redux store between windows? - javascript

I have an electron app based on electron-react-boilerplate.
Now, that I have one window running as I wanted it to run, I started to create a new window.
I currently have 2 html files - one for each window - containing div roots:
<div data-root id="main_root"></div>
<div data-root id="second_root"></div>
My index.js file that is response for rendering the react app looks like this:
import React from 'react';
import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import HomeRoot from './roots/HomeRoot';
import HoverRoot from './roots/HoverRoot';
import { configureStore, history } from './store/configureStore';
const store = configureStore();
const rootMapping = {
main_root: {
name: 'HomeRoot',
Component: HomeRoot,
getNextRoot: () => require('./roots/HomeRoot'),
},
second_root: {
name: 'SecondRoot',
Component: SecondRoot,
getNextRoot: () => require('./roots/SecondRoot'),
},
};
const renderDesiredRoot = () => {
const rootElementID = document.querySelector('[data-root]').id;
const root = rootMapping[rootElementID];
if (!root) throw Error('There is no such Root component!');
const { Component, getNextRoot, name } = root;
render(
<AppContainer>
<Component store={store} history={history} />
</AppContainer>,
document.getElementById(rootElementID),
);
if (module.hot) {
module.hot.accept(`./roots/${name}`, () => {
const NextRoot = getNextRoot();
render(
<AppContainer>
<NextRoot store={store} history={history} />
</AppContainer>,
document.getElementById(rootElementID),
);
});
}
};
renderDesiredRoot();
What it does, it checks which div root is available, and renders proper components.
My problem
How can I make a store that will be shared accross the BrowserWindow instances? I already looked into 2 npm packages (electron-redux and redux-electron-store) and they do not seem as a solution for me in this case.

I tried using this very simple approach, it works almost perfectly, but sometimes it's freezing (I'm not sure yet what exactly is making it to freeze). Maybe this could be useful to anyone, and if someone finds out what is causing the freezing issue, please let us know.
Redux store code (this same code is used by all windows):
export const store = window.opener?.store || createStore(...);
Object.assign(window, { store });
Then I need to open new electron window from a renderer process of the main window using:
const newWindow = window.open("/path", "someName");
And we also need this code on the main process:
win.webContents.on("new-window", function (e, url, frameName, _, options) {
e.preventDefault();
if (frameName === "someName")
e.newGuest = new BrowserWindow({ ...options, width: 300, height: 200, /* anything else you wanna add */ });
});

Nice solution:
You can use redux-state-sync which is a redux middleware that used to synchronize the redux store and actions across multiple react tabs, which works nicely with electron as the tabs here are the different renderer process.
The only hindrance is to initialize the store in the newly created window, that can be done by sending the store state with an ipc call from the main window that opens the new one, that dispatches an action to update the state.
This initialization approach works nicely in react#17.0.0 , but for some reason it doesn't in react react#18.0.0

Related

How to implement Broadcast Channel API in React

I need to check when the user opens a new tab if there any other tabs are opened in the browser. So when we can able to find that there are no tabs opened already, then we need to do some operations if not we can just leave
How can we achieve this using Broadcast Channel API?
Especially how to implement this concept in React?
Thanks in Advance!!
I will answer the second part of your question "Especially how to implement this concept in React?"
I will give an example of implementing multi-tab logout.
Create a file somewhere in your App , I created mine in a folder called Auth and created a file named auth.js
import { BroadcastChannel } from 'broadcast-channel';
const logoutChannel = new BroadcastChannel('logout');
export const login = () => {
localStorage.setItem("token", "this_is_a_demo_token")
history.push('/app/dashboard')
}
export const logout = () => {
logoutChannel.postMessage("Logout")
localStorage.removeItem("token", 'this_is_a_demo_token' )
window.location.href = window.location.origin + "/";
}
export const logoutAllTabs = () => {
logoutChannel.onmessage = () => {
logout();
logoutChannel.close();
}
}
As you can see, I use this dependency npm i broadcast-channel for simplicity with my React App.
Create an instance called logoutChannel with the name 'logout'. On logging out , the instance sends a post message ('Logout').
Use the logoutAllTabs function in your App.js file as follows
import React, { useEffect } from "react";
import { logoutAllTabs } from "./auth/auth";
import Router from "./routes";
function App() {
useEffect(() => {
logoutAllTabs()
}, [])
return (
<>
<Router/> // All routes here
</>
);
}
export default App;
Kindly follow this tutorials to see the above implementation in action :
1.) https://youtu.be/mb5nuUbvfvM
2.) https://dev.to/demawo/how-to-logout-of-multiple-tabs-react-web-app-2egf

Proper way of passing asynchronous data in nextjs to pages

current directory setup:
- components
- NavBar
- Header
- Layout
- pages
- pages
- demo.js
- _app.js
- index.js
// index.js
import React from 'react';
import NewLayout from "../../components/NewLayout/NewLayout.js";
import $nacelle from '../../services/nacelle';
const Index = ({page}) => (
<>
<NewLayout header={page} />
<pre>{JSON.stringify(page.id, null, 2)}</pre>
</>
);
export async function getStaticProps({ context }) {
try {
const page = await $nacelle.data.content({
handle: 'header_us_retail',
type: 'header'
});
return {
props: { page }
};
} catch {
// do something useful if it doesnt work
const page = 'failed';
return {
props: { page }
};
}
}
export default Index;
I am importing Layout into the index.js file, loading asynchronous data and passing it to layout as props that will then be used to render the header and navbar (which are imported by the layout component). This works as expected in the index file, however I want this same functionality to work in the demo.js file and any other file I create in pages or elsewhere. Most likely the issue is how I'm trying to use Nextjs and React (new to both), any help would be greatly appreciated.
Turns out the issue was with how Nacelle was accessing the environment variables, so not a NextJS, or React issue.
According to the devs there are multiple ways to expose the environment variables and the following method solved my particular issue:
// file in root: next.config.js
module.exports = {
env: {
NACELLE_SPACE_ID: process.env.NACELLE_SPACE_ID,
NACELLE_GRAPHQL_TOKEN: process.env.NACELLE_GRAPHQL_TOKEN,
NACELLE_ENDPOINT: process.env.NACELLE_ENDPOINT,
},
};

React/Node app not working on Chrome "Error running template: Invariant Violation: Invalid hook call"

I am getting an error in my react/node/meteor application. In particular, the app fails to load on Chrome, but works properly on all other browsers (edge, firefox). On Chrome I get a 500 error and the page does not load. On the terminal, where I am running the app, I get this:
(webapp_server.js:1010) Error running template: Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks...
I know that there are three common causes of this error as we see here:
https://reactjs.org/warnings/invalid-hook-call-warning.html
But I am using "react": "^16.8.0", "react-dom": "^16.8.0", and I don't use react native.
I don't believe I am improperly using hooks.
I ran this:
meteor npm ls react
and got back the following response:
`-- react#16.8.6
I have reset my machine and still the problem persists: Edge works fine, Chrome fails to load.
Here is the code that is having the error:
import React from 'react';
import MeteorLoadable from 'meteor/nemms:meteor-react-loadable';
import acceptLanguage from 'accept-language';
import { renderToString } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';
import { Meteor } from 'meteor/meteor';
import { onPageLoad } from 'meteor/server-render';
import { ApolloClient } from 'apollo-client';
import { ApolloProvider, getDataFromTree } from 'react-apollo';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { StaticRouter, Route } from 'react-router-dom';
import Helmet from 'react-helmet';
import fetch from 'node-fetch';
import App from '/app/ui/components/smart/app';
import HeaderTitle from '/app/ui/components/smart/header/header-title';
import LanguagePicker from '/app/ui/components/dumb/language-picker';
import Routes from '/app/ui/routes';
import { primaryLocale, otherLocales } from '/app/intl';
const locales = primaryLocale ? [primaryLocale, ...otherLocales] : otherLocales;
acceptLanguage.languages(locales);
const render = async (sink) => {
const ssrClient = new ApolloClient({
link: new HttpLink({
uri: Meteor.absoluteUrl('/graphql'),
fetch,
}),
cache: new InMemoryCache(),
ssrMode: true,
});
const preferredLocale = acceptLanguage.get(sink.request.headers['accept-language']);
let locale = otherLocales.find(l => sink.request.url.path.startsWith(`/${l}`));
let prefix = locale;
// /app-shell is a special route that does no server-side rendering
// It's used by the service worker for all navigation routes, so after the first visit
// the initial server response is very quick to display the app shell, and the client
// adds in the data.
// In the case of a first visit or a robot, we render everything on the server.
if (sink.request.url.path === '/app-shell') {
sink.appendToBody(`<script>window.__APOLLO_STATE__=${JSON.stringify(ssrClient.extract())};</script>`);
sink.appendToBody(`<script>window.__PREFERRED_LOCALE__='${preferredLocale}';</script>`);
sink.appendToBody(MeteorLoadable.getLoadedModulesScriptTag());
return;
}
// We first check if we need to redirect to a locale
// We can only do this is there isn't a primary locale.
if (!primaryLocale) {
if (!locale) {
sink.setStatusCode(307);
sink.redirect(`/${preferredLocale || otherLocales[0]}${sink.request.url.path}`);
return;
}
} else if (!locale) {
// If there's no locale prefix, we use the primaryLocale instead
locale = primaryLocale;
prefix = '';
}
const ServerApp = ({ component, context }) => (
<MeteorLoadable.Capture>
<StaticRouter location={sink.request.url} context={context}>
<ApolloProvider client={ssrClient}>
<Route
path={`/${prefix}`}
render={props => <App component={component} {...props} locale={locale} section="app" />}
/>
</ApolloProvider>
</StaticRouter>
</MeteorLoadable.Capture>
);
// Load all data from local server
const context = {};
await getDataFromTree(<ServerApp component={Routes} context={context} />);
// Elements that we want rendered on the server
const sheet = new ServerStyleSheet();
sink.renderIntoElementById('header-title', renderToString(sheet.collectStyles(<ServerApp component={HeaderTitle} context={context} />)));
sink.renderIntoElementById('header-lang-picker', renderToString(sheet.collectStyles(<ServerApp component={LanguagePicker} context={context} />)));
sink.renderIntoElementById('main', renderToString(sheet.collectStyles(<ServerApp component={Routes} context={context} />)));
// Append helmet and styles
const helmetResult = Helmet.renderStatic();
['title', 'meta', 'link', 'script'].forEach(k => sink.appendToHead(helmetResult[k].toString()));
sink.appendToHead(sheet.getStyleTags());
// Append user's preferred locale
sink.appendToBody(`<script>window.__PREFERRED_LOCALE__='${preferredLocale}';</script>`);
// Append Apollo data
sink.appendToBody(`<script>window.__APOLLO_STATE__=${JSON.stringify(ssrClient.extract())};</script>`);
// Append preloaded ReactLoadabe modules
sink.appendToBody(MeteorLoadable.getLoadedModulesScriptTag());
};
Meteor.startup(async () => {
await MeteorLoadable.preloadComponents();
onPageLoad(render);
});
In particular it is this line that is returning the error:
await getDataFromTree(<ServerApp component={Routes} context={context} />);
I commented that line out and the app seems to work fine now. I don't know if I need it and it should not be causing problems. This code is copied line for line from a starter kit and I don't know what this code is doing.
Note:
This code is taken from the following starter kit:
https://github.com/timothyarmes/ta-meteor-apollo-starter-kit
It looks like this did disobey hooks and so I changed the following code:
await getDataFromTree(<ServerApp component={Routes} context={context} />);
To this:
const aServerApp = () => (
<ServerApp component={Routes} context={context} />
);
await getDataFromTree(aServerApp);
and it seems to work fine.

Duplicate componentDidMount logic with useEffect() to load external JavaScript on client side

I’m implementing a rich text editor into a NextJS project. There are no React components for it, and it runs only on the client side, so I have to load the JavaScript and CSS files from an external source and work around SSR. Please don't recommend to use another tool, as that is not an option.
The tool works fine as a class component, but I’d like to port it into a functional component. When I test the functional component, it works occasionally — namely, after I change my file and save (even if it's just adding a space). But as soon as I refresh the page, I lose the editor. I thought it was because the component hadn’t mounted, but now I check for that, yet the issue persists.
I’ve tried various approaches, including Next’s Dynamic import with SSR disabled, but so far only the class method below has worked (the editor works by binding to the <textarea> element):
import React from "react";
import Layout from "../components/Layout";
class Page extends React.Component {
state = { isServer: true };
componentDidMount() {
this.MyEditor = require("../public/static/cool-editor.js");
this.setState({ isServer: false }); // Trigger rerender.
var app = MyEditor("entry"); // Create instance of editr.
}
render(props) {
return (
<Layout>
<textarea id="entry"></textarea>
</Layout>
);
}
}
export default Page;
Last attempt at functional component:
import React, { useEffect } from "react";
import Layout from "../components/Layout";
function hasWindow() {
const [isWindow, setIsWindow] = React.useState(false);
React.useEffect(() => {
setIsWindow(true);
return () => setIsWindow(false);
}, []);
return isWindow;
}
const Editor = () => {
useEffect(() => {
const script = document.createElement("script");
script.src =
"http://localhost:3000/static/article-editor/cool-editor.js";
script.async = true;
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
}, []);
var app = MyEditor("entry");
return (
<Layout>
<textarea id="entry"></textarea>
</Layout>
);
};
const Page = () => {
const isWindow = hasWindow();
if (isWindow) return <Editor />;
return null;
};
export default Page;
You can use useRef hook in <textarea> tag:
const refContainer = useRef(null);
return <textarea ref={refContainer}>
then useEffect to check if the the element has been mounted.
useEffect(() => {
if (refContainer.current) {
refContainer.current.innerHTML = "ref has been mounted";
console.log("hello");
}
}, []);
Check the code here: https://codesandbox.io/s/modest-dubinsky-7r3oz
Some of the things I could suggest changing:
var app = MyEditor("entry"); is being created on every render. Consider using useRef as a way to keep instance variable: https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables
In Editor, the MyEditor variable is not defined.
hasWindow includes a useEffect that runs once (with empty dependency array), I don't think it needs the clean up function. To check staying at browser or server, you could simply use const isServer = type of window === 'undefined'
Custom hook should be named with prefix use

Redux - Loading initial state asynchronously

I'm trying to work out the cleanest way to load the initial state of my Redux stores when it comes from API calls.
I understand that the typical way of providing the initial state is to generate it server-side on page load, and provide it to Redux createStore() as a simple object. However, I'm writing an app that I'm planning on packaging up in Electron and so this doesn't work.
The best that I've been able to come up with so far is to fire an action immediately after creating the store that will go and request the initial state for the store - either one action that retrieves the entire initial state or a number of actions that each retrieve the initial state for one part of the store. This would then mean that my code looks like:
const store = createStore(reducer, Immutable.Map(), middleware);
store.dispatch(loadStateForA());
store.dispatch(loadStateForB());
store.dispatch(loadStateForC());
Whilst this will work, it seems a bit on the crude side and so I'm wondering if there's some better alternative that I'm missing?
I also encountered the same problem (also building an electron app). A part of my store has application settings which gets persisted on local file system and I needed to load it asynchronously on application's startup.
This is what I come up with. Being a "newbie" with React/Redux, I am very much interested in knowing the thoughts of the community on my approach and how it can be improved.
I created a method which loads the store asynchronously. This method returns a Promise which contains the store object.
export const configureStoreAsync = () => {
return new Promise((resolve) => {
const initialState = initialStoreState;//default initial store state
try {
//do some async stuff here to manipulate initial state...like read from local disk etc.
//This is again wrapped in its own Promises.
const store = createStore(rootReducer, initialState, applyMiddleware(thunk));
resolve(store);
});
} catch (error) {
//To do .... log error!
const store = createStore(rootReducer, initialState, applyMiddleware(thunk));
console.log(store.getState());
resolve(store);
}
});
};
Then in my application entry point, here's how I used it:
configureStoreAsync().then(result => {
const store = result;
return ReactDOM.render(
<Provider store={store}>
<App store={store}/>
</Provider>,
document.getElementById('Main'));
});
Like I said, this is my naive attempt at solving this problem and I am sure there must be better ways of handling this problem. I would be very much interested in knowing how this can be improved.
As far as I can tell, you have only two options (logically):
Set the initial state after the store is instantiated
Set the initial state when the store is instantiated
Option 1 must be done using an action:
The only way to change the state is to emit an action, an object
describing what happened.
— One of "Three Principles" in the docs
This is what you've tried, but you think it is crude for some reason.
The alternative is just to call createStore after your asynch request has resolved. One solution has already been posted (by #Gaurav Mantri) using a Promise object, which is a nice approach.
I would recommend against this, since you will likely have multiple modules trying to require or import your store (or store.dispatch, or store.subscribe) before it exists; they would all have to be made to expect Promises. The first method is the most Redux-y.
My app startup workflow:
Loading spinner in index.html
Ajax to check if user is logged in
On ajax end, render the Root component
Hide the loading spinner
I achieved that by:
Creating the store with a custom middleware that listens for the initial ajax end action and calls a callback once
Dispatching the initial ajax action
root.js
const store = createStore(
rootReducer,
applyMiddleware(
...,
actionCallbackOnceMiddleware(INITIAL_AJAX_END, render)
)
)
function render() {
ReactDOM.render(
<Provider store={store}>
<RootComponent/>
</Provider>,
document.getElementById('root')
)
document.getElementById('loading').dispatchEvent(new Event('hide'))
}
store.dispatch(initialAjaxAction());
middleware/actionCallbackOnce.js
export default (actionType, callback) => store => next => {
let called = false;
return action => {
next(action);
if (!called && action.type === actionType) {
called = true;
callback();
}
}
}
index.html
<div id="loading">
<span>Loading</span>
<style type="text/css">...</style>
<script>
(function(loading){
loading.addEventListener('hide', function(){
loading.remove();
});
loading.addEventListener('error', function(){
loading.querySelector('span').textContent = "Error";
});
})(document.getElementById('loading'));
</script>
</div>
<div id="root"></div>
Using extraReducers with createAsyncThunk seems to be the clean way of doing this as explained here
Using async thunks would give you more control. This approach worked for me. In this example the user has a setting for the UI's theme, and this setting will be persisted to the backend. We can't render the UI until we know this setting.
Add an Async Thunk to a Slice: Here we use createAsyncThunk. Async thunks are actions but with the additional ability to (i) perform an API request, (ii) update the state using results from API request. (I'm assuming here you are using redux slices, if you are not then just add this thunk to your main reducer).
// ./store/settings.js
import {
createAsyncThunk,
createReducer,
} from '#reduxjs/toolkit';
import { client } from './api/client';
const initialState = {
theme: 'light', // can be either 'light', 'dark' or 'system'
};
const fetchSettings = createAsyncThunk('settings/fetchSettings', async () => {
const response = await client.fetch('/api/v1/settings');
// `response` is an object returned from server like: { theme: 'dark' }
return response;
});
const settingsReducer = createReducer(initialState, builder => {
builder.addCase(fetchSettings.fulfilled, (state, action) => {
state.theme = action.payload.theme;
});
});
export { fetchSettings };
export default settingsReducer;
Combine Reducers: With slices your state is divided up and so you'll be bringing all your reducers together into one single reducer (some redux boilerplate has bene replaced with // ...):
// ./store/index.js
// ...
// import fooReducer from './store/foo';
// import barReducer from './store/bar';
import settingsReducer from './store/settings';
export const store = configureStore({
reducer: {
// foo: fooReducer,
// bar: barReducer,
settings: settingsReducer,
},
});
// ...
export const { useDispatch, useSelector }
Dispatch Thunk: Dispatching the async thunk will perform the API request and update the store. With async thunks you can use await to wait until this is all done. We won't perform the initial render until this is done.
// ./index.js
import App from './components/App';
import { store } from './store/index';
import { fetchSettings } from './store/settings';
async function main() {
await store.dispatch(fetchSettings());
root.render(
<StrictMode>
<App store={store} />
</StrictMode>,
);
}
main();
Render App: The app will use this updated store and render the theme from the backend.
// ./components/App.js
import { useSelector } from './store/index';
export default function App({ store }) {
// read theme from store
const settings = useSelector(state => state.settings);
const settingsTheme = settings.theme;
return (
<Provider store={store}>
<div>Your app goes here. The theme is ${settingsTheme}</div>
</Provider>
);
}

Categories

Resources