I have a component like so
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { useParams } from 'react-router-dom';
import PodcastActions from '../../store/podcast/podcast.actions';
const selectPodcast = createSelector(
state => state.podcasts,
(_, id) => id,
(podcasts, id) => {
return podcasts
? podcasts.find(podcast => {
return podcast.id.toString() === id;
})
: null;
});
const Podcast = () => {
const dispatch = useDispatch();
const params = useParams();
const podcast = useSelector(state => selectPodcast(state, params.id));
useEffect(() => {
if (!podcast) {
dispatch(PodcastActions.getPodcastById(params.id));
}
}, [dispatch, podcast, params.id]);
return <h2>{podcast.title}</h2>;
};
//Here I need to get the id
Podcast.serverFetch = PodcastActions.getPodcastById(); //Server side render - this is on refresh of the page
export default Podcast;
So I need to get the podcastId outside of the component for my SSR to be able to fetch using this
const dataRequirements =
routes
.filter(route => matchPath(req.url, route)) // filter matching paths
.map(route => route.component) // map to components
.filter(comp => comp.serverFetch) // check if components have data requirement
.map(comp => store.dispatch(comp.serverFetch())); // dispatch data requirement
But how can I do this?
For reference the url looks like this
localhost:port/podcasts/:id
I eventually changed the datarequirements function like follows
app.get("/*", async (req, res) => {
const context = {};
const store = createStore();
const dataRequirements = routes
.filter(route => matchPath(req.url, route)) // filter matching paths
.map(route => {
console.dir(route);
if (route.component?.serverFetch) {
const params = {};
const parts = route.path.split(`/`);
const routeParts = req.url.split(`/`);
parts.forEach((part, index) => {
if (part.startsWith(`:`)) {
params[part.substr(1)] = routeParts[index];
}
});
return store.dispatch(route.component.serverFetch(params));
}
});
//REST OF THE LOGIC TO RENDER REACT SSR
hope this will help someone else.
For reference, my routes are built in a constants file like follows
{
path: "pathOfRoute",
exact: true,
component: ReactComponentReference
}
Related
So I am using a context provider to give the base data to my app for example: [{id: "a", name: "a"}].
Now I have a component that required the data portion of this object, I check if this data property is not yet there, I get it from my api, fill it and then it should not have to recall the api to get it again.
My provider:
import { createContext, useState, useCallback, useMemo, useContext } from "react";
import axios from "axios";
export const StockContext = createContext();
export const useStock = () => useContext(StockContext);
export const StockProvider = ({ children }) => {
const [stocks, setStocks] = useState();
const getDataFromStock = useCallback(
async ({ id }) => {
let method = "GET";
let url = `${config.base_url}data/${id}`;
try {
const { data: response } = await axios({ method, url });
const { succes, data, error } = response;
let updated = stocks;
updated.find((s) => s.id === id).data = data;
console.log("set stocks", updated);
setStocks(updated);
}
return succes;
}
},
[stocks]
);
const value = useMemo(
() => ({
getDataFromStock,
stocks,
}),
[getDataFromStock, stocks]
);
return <StockContext.Provider value={value}>{children}</StockContext.Provider>;
};
Note: I removed some error handling for simplicity sake.
After the function getDataFromStock with the id is called. The stocks object should look like this: [{id: "a", name: "a", data: [{id: "c", ...}]}].
Now I have my component to show the details (data) from this object. It first checks if it is not already in the 'stocks' object and if not gets it.
export default function StockDetailPage() {
const { id } = useParams();
const [currentStock, setCurrentStock] = useState({});
const { stocks, getDataFromStock, testStocks } = useContext(StockContext);
// TODO find why data keep disappearing
useEffect(() => {
const getData = async () => {
if (!stocks.find((s) => s.id === id).data) {
console.log("getting");
await getDataFromStock({ id });
// TODO Indication on loading?
} else {
console.log("not getting");
}
};
getData();
}, [getDataFromStock, id, stocks]);
return (
<div>
Stock: {currentStock?.owner?.username}
{stocks
?.find((s) => s.id === id)
.data?.map((p) => (
<ProductPreview key={p.product_id} {...p} />
))}
</div>
);
}
Now if I look at my react debugger, I see the state of the stocks has this data object, but once I navigate away from the details, this property seems to disappear. Now how could I implement this in the right way or fix the problem?
Kind regards
How do we detect a change in the URL hash of a Next.js project?
I don't want to reload my page every time the slug changes.
I cannot use <Link> since all of my data comes from DB
Example:
When clicking on an tag from
http://example/test#url1
to
http://example.com/test#url2
Tried the below, but this seems to work for path change only.
import React, { useEffect,useState } from 'react';
import { useRouter } from 'next/router'
const test = () => {
const router = useRouter();
useEffect(() => {
console.log(router.asPath);
}, [router.asPath]);
return (<></>);
};
export default test;
You can listen to hash changes using hashChangeStart event from router.events.
const Test = () => {
const router = useRouter();
useEffect(() => {
const onHashChangeStart = (url) => {
console.log(`Path changing to ${url}`);
};
router.events.on("hashChangeStart", onHashChangeStart);
return () => {
router.events.off("hashChangeStart", onHashChangeStart);
};
}, [router.events]);
return (
<>
<Link href="/#some-hash">
<a>Link to #some-hash</a>
</Link>
<Link href="/#some-other-hash">
<a>Link to #some-other-hash</a>
</Link>
</>
);
};
If you're not using next/link or next/router for client-side navigation (not recommended in Next.js apps), then you'll need to listen to the window's hashchange event.
Your useEffect would look like the following.
useEffect(() => {
const onHashChanged = () => {
console.log('Hash changed');
};
window.addEventListener("hashchange", onHashChanged);
return () => {
window.removeEventListener("hashchange", onHashChanged);
};
}, []);
If you're relying on URL hash for multiple re-renders or state changes, note that NextJS hashChangeStart event does not account for browser refresh or direct browser URL address navigation
A complete solution might need a combination of event listeners to cover all edge cases.
const useUrlHash = (initialValue) => {
const router = useRouter()
const [hash, setHash] = useState(initialValue)
const updateHash = (str) => {
if (!str) return
setHash(str.split('#')[1])
}
useEffect(() => {
const onWindowHashChange = () => updateHash(window.location.hash)
const onNextJSHashChange = (url) => updateHash(url)
router.events.on('hashChangeStart', onNextJSHashChange)
window.addEventListener('hashchange', onWindowHashChange)
window.addEventListener('load', onWindowHashChange)
return () => {
router.events.off('hashChangeStart', onNextJSHashChange)
window.removeEventListener('load', onWindowHashChange)
window.removeEventListener('hashchange', onWindowHashChange)
}
}, [router.asPath, router.events])
return hash
}
I'm trying to make a page to show the details of each video.
I fetched multiple video data from the back-end and stored them as global state.
This code works if I go to the page through the link inside the app. But If I reload or open the URL directory from the browser, It can not load the single video data.
How should I do to make this work?
Thanx
Single Video Page
import { useState, useEffect, useContext } from "react";
import { useParams } from "react-router-dom";
import { VideoContext } from "../context/videoContext";
const SingleVideo = () => {
let { slug } = useParams();
const [videos, setVideos] = useContext(VideoContext);
const [video, setVideo] = useState([]);
useEffect(() => {
const result = videos.find((videos) => {
return videos.uuid === slug;
});
setVideo((video) => result);
}, []);
return (
<>
<div>
<h1>{video.title}</h1>
<p>{video.content}</p>
<img src={video.thumbnail} alt="" />
</div>
</>
);
};
export default SingleVideo;
Context
import React, { useState, createContext, useEffect } from "react";
import Axios from "axios";
import { AxiosResponse } from "axios";
export const VideoContext = createContext();
export const VideoProvider = (props) => {
const [videos, setVideos] = useState([]);
const config = {
headers: { "Access-Control-Allow-Origin": "*" },
};
useEffect(() => {
//Fetch Vidoes
Axios.get(`http://localhost:5000/videos`, config)
.then((res: AxiosResponse) => {
setVideos(res.data);
})
.catch((err) => {
console.log(err);
});
}, []);
return (
<VideoContext.Provider value={[videos, setVideos]}>
{props.children}
</VideoContext.Provider>
);
};
I think the reason is because when you refresh the app, you fetch the video data on context and the useEffect on your single video page component runs before you receive those data.
To fix you can simply modify slightly your useEffect in your single video component to update whenever you receive those data:
useEffect(() => {
if (videos.length) {
const result = videos.find((videos) => {
return videos.uuid === slug;
});
setVideo((video) => result);
}
}, [videos]);
Case
I want to make isLoading (global state using React Context) value and changeIsLoading function (its changing function from IsLoadingContext.js file) becomes accessible to all files (function components and simple javascript functions).
I know that React Hooks can only be called inside of the body of a function component.
Question: So in my case here, how could I called isLoading and changeIsLoading inside a util file (non-function component or just a simple javascript function)?
What should I change from the code?
Code flow
(location: SummariesPage.js) Click the button inside SummariesPage component
(location: SummariesPage.js) Call onApplyButtonIsClicked function in SummariesPage component
(location: SummariesPage.js) Change isLoading global state into true then call fetchAPISummaries function
(location: fetchAPISummaries.js) Call fetchAPICycles function
(location: fetchAPICycles.js) Call exportJSONToExcel function
(location: exportJSONToExcel.js) Export the JSON into an Excel file then change isLoading global state into false
IsLoadingContextProvider component will be rerendered and the isLoading value in SummariesPage will be true
Error logs
Uncaught (in promise) Error: 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
3. You might have more than one copy of React in the same app
The code
IsLoadingContext.js:
import React, { useState } from 'react'
const IsLoadingContext = React.createContext()
const IsLoadingContextProvider = (props) => {
const [isLoading, setIsLoading] = useState(false)
const changeIsLoading = (inputState) => {
setIsLoading(inputState)
}
return(
<IsLoadingContext.Provider
value={{
isLoading,
changeIsLoading
}}
>
{props.children}
</IsLoadingContext.Provider>
)
}
export { IsLoadingContextProvider, IsLoadingContext }
SummariesPage.js:
import React, { useContext } from 'react'
// CONTEXTS
import { IsLoadingContext } from '../../contexts/IsLoadingContext'
// COMPONENTS
import Button from '#material-ui/core/Button';
// UTILS
import fetchAPISummaries from '../../utils/export/fetchAPISummaries'
const SummariesPage = () => {
const { isLoading, changeIsLoading } = useContext(IsLoadingContext)
const onApplyButtonIsClicked = () => {
changeIsLoading(true)
fetchAPISummaries(BEGINTIME, ENDTIME)
}
console.log('isLoading', isLoading)
return(
<Button
onClick={onApplyButtonIsClicked}
>
Apply
</Button>
)
}
export default SummariesPage
fetchAPISummaries.js:
// UTILS
import fetchAPICycles from './fetchAPICycles'
const fetchAPISummaries = (inputBeginTime, inputEndTime) => {
const COMPLETESUMMARIESURL = .....
fetch(COMPLETESUMMARIESURL, {
method: "GET"
})
.then(response => {
return response.json()
})
.then(responseJson => {
fetchAPICycles(inputBeginTime, inputEndTime, formatResponseJSON(responseJson))
})
}
const formatResponseJSON = (inputResponseJSON) => {
const output = inputResponseJSON.map(item => {
.....
return {...item}
})
return output
}
export default fetchAPISummaries
fetchAPICycles.js
// UTILS
import exportJSONToExcel from './exportJSONToExcel'
const fetchAPICycles = (inputBeginTime, inputEndTime, inputSummariesData) => {
const COMPLETDEVICETRIPSURL = .....
fetch(COMPLETDEVICETRIPSURL, {
method: "GET"
})
.then(response => {
return response.json()
})
.then(responseJson => {
exportJSONToExcel(inputSummariesData, formatResponseJSON(responseJson))
})
}
const formatResponseJSON = (inputResponseJSON) => {
const output = inputResponseJSON.map(item => {
.....
return {...item}
})
return output
}
export default fetchAPICycles
exportJSONToExcel.js
import { useContext } from 'react'
import XLSX from 'xlsx'
// CONTEXTS
import { IsLoadingContext } from '../../contexts/IsLoadingContext'
const ExportJSONToExcel = (inputSummariesData, inputCyclesData) => {
const { changeIsLoading } = useContext(IsLoadingContext)
const sheetSummariesData = inputSummariesData.map((item, index) => {
let newItem = {}
.....
return {...newItem}
})
const sheetSummaries = XLSX.utils.json_to_sheet(sheetSummariesData)
const workBook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workBook, sheetSummaries, 'Summaries')
inputCyclesData.forEach(item => {
const formattedCycles = item['cycles'].map((cycleItem, index) => {
.....
return {...newItem}
})
const sheetCycles = XLSX.utils.json_to_sheet(formattedCycles)
XLSX.utils.book_append_sheet(workBook, sheetCycles, item['deviceName'])
})
XLSX.writeFile(workBook, `......xlsx`)
changeIsLoading(false)
}
export default ExportJSONToExcel
I believe the real problem you are facing is managing the asynchronous calls. It would be much readable if you use async/await keywords.
const onApplyButtonIsClicked = async () => {
changeIsLoading(true)
await fetchAPISummaries(BEGINTIME, ENDTIME)
changeIsLoading(false)
}
You will need to rewrite fetchAPICycles to use async/await keywords instead of promises.
const fetchAPICycles = async (
inputBeginTime,
inputEndTime,
inputSummariesData
) => {
const COMPLETDEVICETRIPSURL = ...;
const response = await fetch(COMPLETDEVICETRIPSURL, {
method: "GET",
});
const responseJson = await response.json();
exportJSONToExcel(inputSummariesData, formatResponseJSON(responseJson));
};
I have a React Native App,
Here i use mobx ("mobx-react": "^6.1.8") and react hooks.
i get the error:
Invalid hook call. Hooks can only be called inside of the body of a function component
Stores index.js
import { useContext } from "react";
import UserStore from "./UserStore";
import SettingsStore from "./SettingsStore";
const useStore = () => {
return {
UserStore: useContext(UserStore),
SettingsStore: useContext(SettingsStore),
};
};
export default useStore;
helper.js OLD
import React from "react";
import useStores from "../stores";
export const useLoadAsyncProfileDependencies = userID => {
const { ExamsStore, UserStore, CTAStore, AnswersStore } = useStores();
const [user, setUser] = useState({});
const [ctas, setCtas] = useState([]);
const [answers, setAnswers] = useState([]);
useEffect(() => {
if (userID) {
(async () => {
const user = await UserStore.initUser();
UserStore.user = user;
setUser(user);
})();
(async () => {
const ctas = await CTAStore.getAllCTAS(userID);
CTAStore.ctas = ctas;
setCtas(ctas);
})();
(async () => {
const answers = await AnswersStore.getAllAnswers(userID);
UserStore.user.answers = answers.items;
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
setAnswers(answers.items);
})();
}
}, [userID]);
};
Screen
import React, { useEffect, useState, useRef } from "react";
import {
View,
Dimensions,
SafeAreaView,
ScrollView,
StyleSheet
} from "react-native";
import {
widthPercentageToDP as wp,
heightPercentageToDP as hp
} from "react-native-responsive-screen";
import { observer } from "mobx-react";
import useStores from "../../stores";
import { useLoadAsyncProfileDependencies } from "../../helper/app";
const windowWidth = Dimensions.get("window").width;
export default observer(({ navigation }) => {
const {
UserStore,
ExamsStore,
CTAStore,
InternetConnectionStore
} = useStores();
const scrollViewRef = useRef();
const [currentSlide, setCurrentSlide] = useState(0);
useEffect(() => {
if (InternetConnectionStore.isOffline) {
return;
}
Tracking.trackEvent("opensScreen", { name: "Challenges" });
useLoadAsyncProfileDependencies(UserStore.userID);
}, []);
React.useEffect(() => {
const unsubscribe = navigation.addListener("focus", () => {
CTAStore.popBadget(BadgetNames.ChallengesTab);
});
return unsubscribe;
}, [navigation]);
async function refresh() {
const user = await UserStore.initUser(); //wird das gebarucht?
useLoadAsyncProfileDependencies(UserStore.userID);
if (user) {
InternetConnectionStore.isOffline = false;
}
}
const name = UserStore.name;
return (
<SafeAreaView style={styles.container} forceInset={{ top: "always" }}>
</SafeAreaView>
);
});
so now, when i call the useLoadAsyncProfileDependencies function, i get this error.
The Problem is that i call useStores in helper.js
so when i pass the Stores from the Screen to the helper it is working.
export const loadAsyncProfileDependencies = async ({
ExamsStore,
UserStore,
CTAStore,
AnswersStore
}) => {
const userID = UserStore.userID;
if (userID) {
UserStore.initUser().then(user => {
UserStore.user = user;
});
CTAStore.getAllCTAS(userID).then(ctas => {
console.log("test", ctas);
CTAStore.ctas = ctas;
});
AnswersStore.getAllAnswers(userID).then(answers => {
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
});
}
};
Is there a better way? instead passing the Stores.
So that i can use this function in functions?
As the error says, you can only use hooks inside the root of a functional component, and your useLoadAsyncProfileDependencies is technically a custom hook so you cant use it inside a class component.
https://reactjs.org/warnings/invalid-hook-call-warning.html
EDIT: Well after showing the code for app.js, as mentioned, hook calls can only be done top level from a function component or the root of a custom hook. You need to rewire your code to use custom hooks.
SEE THIS: https://reactjs.org/docs/hooks-rules.html
You should return the value for _handleAppStateChange so your useEffect's the value as a depdendency in your root component would work properly as intended which is should run only if value has changed. You also need to rewrite that as a custom hook so you can call hooks inside.
doTasksEveryTimeWhenAppWillOpenFromBackgorund and doTasksEveryTimeWhenAppGoesToBackgorund should also be written as a custom hook so you can call useLoadAsyncProfileDependencies inside.
write those hooks in a functional way so you are isolating specific tasks and chain hooks as you wish without violiating the rules of hooks. Something like this:
const useGetMyData = (params) => {
const [data, setData] = useState()
useEffect(() => {
(async () => {
const apiData = await myApiCall(params)
setData(apiData)
})()
}, [params])
return data
}
Then you can call that custom hook as you wish without violation like:
const useShouldGetData = (should, params) => {
if (should) {
return useGetMyData()
}
return null
}
const myApp = () => {
const myData = useShouldGetData(true, {id: 1})
return (
<div>
{JSON.stringify(myData)}
</div>
)
}