How do I read from local storage in Ionic React? - reactjs

I'm new to React and Ionic as well as JS in general so I've followed the tutorial and tried to adapt it to my use case.
What I want to achieve is the following:
read a JSON string from local storage when the app loads
write the (newer) JSON back to storage when the app quits
What I have right now is (heavily truncated):
ExploreContainer.tsx
import { useStore } from '../hooks/useFilesystem';
var vars = { intervaltime : 50000 /* ... */ };
const ExploreContainer: React.FC<ContainerProps> = ({ name }) => {
const { writeVars, readVars } = useStore();
writeVars();
if ( vars.intervaltime == 50000 ) {
vars = JSON.parse( readVars() );
}
console.log( vars );
// ...
useFilesystem.ts
import { useStorage } from '#ionic/react-hooks/storage';
import { vars } from '../components/ExploreContainer';
const FILESTORAGE = "files";
export function useStore() {
const { get, set } = useStorage();
const writeVars = async () => {
set(FILESTORAGE, JSON.stringify(vars));
};
const readVars = function() {
get(FILESTORAGE).then((value) => { return value });
}
return {
writeVars,
readVars
};
};
The problem right now:
The readVars() call in the React.FC doesn't wait for the get in the custom hook. When I log the output on read, I see that it's an unfulfilled promise. Naturally, this prompts the JSON.parse() to throw an error, because the string it could parse isn't read yet.
I tried to declare an async function in the React.FC so I could await it, but function declarations are not possible because the FC is in strict mode always.
I also tried to declare the async function before the React.FC but this doesn't work as well, because I mustn't call the hook outside the FC if I interpreted the error messages correctly.
In the custom hook I previously had an await statement instead of the .then, but the behavior was the same.
If there is an easier way to read and write a JSON string into some form of persistent(!) storage, I'd be happy to learn about it!

I see this as classic async/await issue only.
As you declared function 'writeVars' as async, so I expect function 'set()' is async in nature. If so please add await in front of 'set' function.
If 'get' function also an async one, then add await in front of it and add 'async' to 'readVars' function declaration.
If point 1 and point 2 are correct, then add await in front of functions 'writeVars' and 'readVars' where ever you call them and add async to the function declaration of calling function.

The problem was not being able to await in the React.FC-block.
However, the following worked like a charm:
var readPromise = readVars();
readPromise.then(function(result) {
vars = JSON.parse(result);
//doing stuff to the data
});
This ensured that the JSON.parse(result) only executes when the readVars-promise has been successfully resolved.

Related

How do I separate api / async request logic from react components when using recoil

So at the moment I am having to put my request / api logic directly into my components because what I need to do a lot of the time is set state based on the response I get from the back end.
Below is a function that I have on my settings page that I use to save the settings to recoil after the user hits save on the form:
const setUserConfig = useSetRecoilState(userAtoms.userConfig);
const submitSettings = async (values: UserConfigInterface) => {
try {
const { data: {data} } = await updateUser(values);
setUserConfig({
...data
});
} catch (error) {
console.log('settings form error: ', error);
}
}
This works perfectly...I just dont want the function in my component as most of my components are getting way bigger than they need to be.
I have tried making a separate file to do this but I can only use the recoil hooks (in this instance useSetRecoilState) inside of components and it just complains when I try and do this outside of a react component.
I have tried implementing this with recoils selector and selectorFamily functions but it gets kind of complicated. Here is how I have tried it inside of a file that has atoms / selectors only:
export const languageProgress = atom<LanguageProgress>({
key: "LanguageProgress",
default: {
level: 1,
xp: 0,
max_xp: 0
}
})
export const languageProgressUpdate = selectorFamily<LanguageProgress>({
key: "LanguageProgress",
get: () => async () => {
try {
const { data: { data } } = await getLanguageProgress();
return data;
} catch (error) {
console.log('get language progress error');
}
},
set: (params:object) => async ({set}) => {
try {
const { data: { data } } = await updateLanguageProgress(params);
set(languageProgress, {
level: data.level,
xp: data.xp,
max_xp: data.max_xp
});
} catch (error) {
console.log('language progress update error: ', error);
}
}
});
What I want to do here is get the values I need from the back end and display it in the front which I can do in the selector function get but now I have 2 points of truth for this...my languageProgress atom will initially be incorrect as its not getting anything from the database so I have to use useGetRevoilValue on the languageProgressUpdate selector I have made but then when I want to update I am updating the atom and not the actual value.
I cannot find a good example anywhere that does what I am trying to here (very suprisingly as I would have thought it is quite a common way to do things...get data from back end and set it in state.) and I can't figure out a way to do it without doing it in the component (as in the first example). Ideally I would like something like the first example but outside of a component because that solution is super simple and works for me.
So I dont know if this is the best answer but it does work and ultimately what I wanted to do was seperate the logic from the screen component.
The answer in my situation is a bit long winded but this is what I used to solve the problem: https://medium.com/geekculture/crud-with-recoiljs-and-remote-api-e36581b77168
Essentially the answer is to put all the logic into a hook and get state from the api and set it there.
get data from back end and set it in state
You may be looking for useRecoilValueLoadable:
"This hook is intended to be used for reading the value of asynchronous selectors. This hook will subscribe the component to the given state."
Here's a quick demonstration of how I've previously used it. To quickly summarise: you pass useRecoilValueLoadable a selector (that you've defined somewhere outside the logic of the component), that selector grabs the data from your API, and that all gets fed back via useRecoilValueLoadable as an array of 1) the current state of the value returned, and 2) the content of that API call.
Note: in this example I'm passing an array of values to the selector each of which makes a separate API call.
App.js
const { state, contents } = useRecoilValueLoadable(myQuery(arr));
if (state.hasValue && contents.length) {
// `map` over the contents
}
selector.js
import { selectorFamily } from 'recoil';
export const myQuery = selectorFamily({
key: 'myQuery',
get: arr => async () => {
const promises = arr.map(async item => {
try {
const response = await fetch(`/endpoint/${item.id}`);
if (response.ok) return response.json();
throw Error('API request not fulfilled');
} catch (err) {
console.log(err);
}
});
const items = await Promise.all(promises);
return items;
}
});

Why "React hook is called in a function" while exporting simple single function that accesses a useAtom state?

I have this simple function that needs to set a state from jotai. I would like this function to have it's own seperate file or be cleaned up somewhere, since it will be reused. I'm new to React and come from Angular. It's kind of a function in a service Angular wise.
How would you solve this properly in React?
Code:
export const setMetamaskWallet = (): void => {
const [, setWeb3] = useAtom(web3Atom);
const [, setLoading] = useAtom(loadingAtom);
const [wallet, setWallet] = useAtom(walletAtom);
setWeb3(
new Web3(window.ethereum),
)
//Todo: Create check if metamask is in browser, otherwise throw error
const setAccount = async () => {
setLoading(true);
const accounts = await window.ethereum.request(
{
method: 'eth_requestAccounts'
},
);
setWallet(
accounts[0],
);
setLoading(false);
}
if (!wallet) {
setAccount();
}
}
Hooks can only be called in React functional components or other hooks. Here it appears you have called it in a typical function, hence the error. You could package this functionality in a custom hook. It may however be most appropriate to keep this function as it is, and instead of calling hooks within it, pass the relevant data into the function as parameters.

How to setup a function which gets app settings and sets it as localStorage before the page loads. (next.js)

I've been working on a Next.JS web application for the past couple of days but I've reached a problem. The app has an API call (/api/settings) which returns some settings about the application from the database. Currently, I have a function which returns these settings and access to the first component:
App.getInitialProps = async () => {
const settingsRequest = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/settings`
);
const settingsResponse = await settingsRequest.json();
return { settings: settingsResponse };
};
This does work and I am able to pass in settings to components but there are two problems with this:
I need to nest the prop through many components to reach the components that I need
This request runs every time a page is reloaded/changed
Essentially, I need to create a system that does this:
runs a function in the _app.tsx getInitialProps to check if the data is already in localStorage, if not make the API request and update localStorage
have the localStorage value accessible from a custom hook.
Right now the problem with this is that I do not have access to localStorage from the app.tsx getInitialProps. So if anyone has an alternative to run this function before any of the page loads, please let me know.
Thanks!
I found a solution, it might be a janky solution but I managed to get it working and it might be useful for people trying to achieve something similar:
First we need to create a "manager" for the settings:
export const checkIfSettingsArePresent = () => {
const settings = localStorage.getItem("app_settings");
if (settings) return true;
return false;
};
export const getDataAndUpdateLocalStorage = async () => {
const r = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/settings`);
const response = await r.json();
localStorage.setItem("app_settings", JSON.stringify(response));
};
With that created we can add a UseEffect hook combined with a useState hook that runs our function.
const [doneFirst, setDoneFirst] = useState<boolean>(false);
useEffect(() => {
const settingsPreset = checkIfSettingsArePresent();
if (performance.navigation.type != 1)
if (settingsPreset) return setDoneFirst(true);
const getData = async () => {
await getDataAndUpdateLocalStorage();
setDoneFirst(true);
};
getData();
}, []);
//any other logic
if (!doneFirst) {
return null;
}
The final if statement makes sure to not run anything else before the function.
Now, whenever you hot-reload the page, you will see that the localStorage app_settings is updated/created with the values from the API.
However, to access this more simply from other parts of the app, I created a hook:
import { SettingsType } from "#sharex-server/common";
export default function useSettings() {
const settings = localStorage.getItem("app_settings") || {
name: "ShareX Media Server",
};
//#ts-ignore
return JSON.parse(settings) as SettingsType;
}
Now I can import useSettings from any function and have access to my settings.

React Hooks: Referencing data that is stored inside context from inside useEffect()

I have a large JSON blob stored inside my Context that I can then make references to using jsonpath (https://www.npmjs.com/package/jsonpath)
How would I go about being able to access the context from inside useEffect() without having to add my context variable as a dependency (the context is updated at other places in the application)?
export default function JsonRpc({ task, dispatch }) {
const { data } = useContext(DataContext);
const [fetchData, setFetchData] = useState(null);
useEffect(() => {
task.keys.forEach(key => {
let val = jp.query(data, key.key)[0];
jp.value(task.payload, key.result_key, val);
});
let newPayload = {
jsonrpc: "2.0",
method: "call",
params: task.payload,
id: "1"
};
const domain = process.env.REACT_APP_WF_SERVER;
let params = {};
if (task.method === "GET") {
params = newPayload;
}
const domain_params =
JSON.parse(localStorage.getItem("domain_params")) || [];
domain_params.forEach(e => {
if (e.domain === domain) {
params[e.param] = e.value;
}
});
setFetchData({ ...task, payload: newPayload, params: params });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [task]);
}
I'm gonna need to post an answer because of code, but I'm not 100% sure about what you need, so I'll build a correct answer with your feedback :)
So, my first idea is: can't you split your effects in two React.useEffect? Something like this:
export default function JsonRpc({ task, dispatch }) {
...
useEffect(() => {
...
setFetchData(...);
}, [task]);
useEffect(() => {
...
}, [data]);
..
}
Now, if my understanding are correct, this is an example of events timeline:
Due to the update on task you will trigger the first useEffect, which can setFetchData();
Due to the update on fetchData, and AXIOS call is made, which updates data (property in the context);
At this, you enter the second useEffect, where you have the updated data, but NO call to setFetchData(), thus no loop;
Then, if you wanted (but couldn't) put data in the dependencies array of your useEffect, I can imagine the two useEffect I wrote have some shared code: you can write a common method called by both useEffects, BUT it's important that the setFetchData() call is outside this common method.
Let me know if you need more elaboration.
thanks for your reply #Jolly! I found a work around:
I moved the data lookup to a state initial calculation:
const [fetchData] = useState(processFetchData(task, data));
then im just making sure i clear the component after the axios call has been made by executing a complete function passed to the component from its parent.
This works for now, but if you have any other suggestions id love to hear them!

Promises inside WebWorker (react) throwing babel/webpack error

I'm trying to offload some intensive data processing to a WebWorker in a react app. If I call any asynchronous function within the onmessage handler, using promises or async/await, I get:
ReferenceError:
_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_0___default is not defined
Here's my worker:
const DataSyncWorker = () => {
self.doSyncing = async () => {
return null;
};
self.onmessage = e => {
if (!e) return;
console.log(`worker received message in listener callback: ${e.data}`);
self.doSyncing();
self.postMessage(`SYNC_COMPLETE`);
};
};
export default DataSyncWorker;
And the setup file for creating the Worker:
export default class WebWorker {
constructor(worker) {
const code = worker.toString();
const blob = new Blob([`(${code})()`]);
return new Worker(URL.createObjectURL(blob));
}
}
And invoking it:
import DataSyncWorker from './workers/DataSyncWorker';
import WebWorker from './workers/workerSetup';
const App = () => {
const dataSyncWorker = new WebWorker(DataSyncWorker);
dataSyncWorker.postMessage(`START_SYNC`);
dataSyncWorker.addEventListener(`message`, event => {
console.log(`index: message from worker: ${e.data}`);
});
}
If I change doSyncing to not be async, everything works fine. This is a simplified example, which I've confirmed still exhibits the same behavior. But I'm not able to use axios, or any other async function. It's my understanding that this should be possible, and, given the error, I am wondering if my issue is related to babel/webpack. Or perhaps I'm doing something else wrong. Any help is greatly appreciated. Thanks.
I solved this with a few changes:
First, I incorporated worker-loader library, using the inline type import, like:
import DataSyncWorker from 'worker-loader!./workers/DataSyncWorker';
And then, I had to unwrap the inner functions from the containing method of the original implementation, so DataSyncWorker now looks like this:
doSyncing = async () => {
return null;
};
self.onmessage = e => {
if (!e) return;
console.log(`worker received message in listener callback: ${e.data}`);
doSyncing();
self.postMessage(`SYNC_COMPLETE`);
};
The other code remains unchanged, and now everything works.
I believe you could use an alternate approach, involving modifying the webpack.config.js, with these additions to the modules.rules section:
{
test: /\.worker\.js$/,
use: ['worker-loader', 'babel-loader'],
include: [path.join(__dirname, 'src/workers')]
},
and then updating the name of your worker file so it's matched by the test condition, and updating it's import, etc, but I haven't tried that yet, and the inline method seemed simpler.

Resources