Javascript array has no length - asynchronous

I am trying to fetch all nfts from different contracts using THIRDWEB. First I am fetching the contract addresses from sanity (which works perfectly fine) and then I am trying to fetch the nfts (you do it by using the useNFTCollection hook and then use the function getOwned).
Although the array that i receive (dataArray) i can see the elements of it (all the NFTs) but I cannot access any of the elements in the array (it always returns undefined) and i cannot access the length of the array.
The array looks like this
Whereas if i try to grab the length of this array it just returns undefined.
The code:
import React, { useState, useEffect } from "react";
import Header from "../../components/Header/Header";
import UserImages from "../../components/Profile/UserImages";
import UserInfo from "../../components/Profile/UserInfo";
import { client } from "../../lib/sanityClient";
import { useNFTCollection } from "#thirdweb-dev/react";
import { useRouter } from "next/router";
export async function getServerSideProps({}) {
const query = `*[_type == "marketItems"] {
contractAddress,
}`;
const data = await client.fetch(query);
return {
props: {
data,
},
};
}
function profile({ data }) {
const [nfts, setNfts] = useState({});
const router = useRouter();
const { wallet } = router.query;
let dataArray = [];
if (data[0] !== undefined) {
if (dataArray === undefined || dataArray.length < 1) {
dataArray = [];
}
for (let i = 0; i < data.length; i++) {
const collection = useNFTCollection(data[i].contractAddress);
async function getNfts() {
if (collection !== undefined) {
await collection.getOwned(wallet).then((res) => {
Promise.all(res).then((values) => {
for (let i = 0; i < values.length; i++) {
dataArray.push(values[i]);
}
});
});
}
}
getNfts();
}
console.log(dataArray);
}
return (
<div>
<Header />
<UserImages />
<UserInfo />
</div>
);
}
export default profile;
How can i access the content of dataArray? And how can i assign it to any state variable? Note that I am using this loop this way because useNFTCollection is a Hook, and it does not allow me to use it in any useEffect or useMemo.
Oh and I am using nextJS in this example.

I solved the problem.
I had to somehow get my data into state, I added a seperate iterator to help me with that (to prevent an infinite loop).
Took me quite long to figure out, it is very simple to create an infinite loop in nextJS/React.

Related

React useState() hook returns initial value [duplicate]

I am trying to learn hooks and the useState method has made me confused. I am assigning an initial value to a state in the form of an array. The set method in useState is not working for me, both with and without the spread syntax.
I have made an API on another PC that I am calling and fetching the data which I want to set into the state.
Here is my code:
<div id="root"></div>
<script type="text/babel" defer>
// import React, { useState, useEffect } from "react";
// import ReactDOM from "react-dom";
const { useState, useEffect } = React; // web-browser variant
const StateSelector = () => {
const initialValue = [
{
category: "",
photo: "",
description: "",
id: 0,
name: "",
rating: 0
}
];
const [movies, setMovies] = useState(initialValue);
useEffect(() => {
(async function() {
try {
// const response = await fetch("http://192.168.1.164:5000/movies/display");
// const json = await response.json();
// const result = json.data.result;
const result = [
{
category: "cat1",
description: "desc1",
id: "1546514491119",
name: "randomname2",
photo: null,
rating: "3"
},
{
category: "cat2",
description: "desc1",
id: "1546837819818",
name: "randomname1",
rating: "5"
}
];
console.log("result =", result);
setMovies(result);
console.log("movies =", movies);
} catch (e) {
console.error(e);
}
})();
}, []);
return <p>hello</p>;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<StateSelector />, rootElement);
</script>
<script src="https://unpkg.com/#babel/standalone#7/babel.min.js"></script>
<script src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
Neither setMovies(result) nor setMovies(...result) works.
I expect the result variable to be pushed into the movies array.
Much like .setState() in class components created by extending React.Component or React.PureComponent, the state update using the updater provided by useState hook is also asynchronous, and will not be reflected immediately.
Also, the main issue here is not just the asynchronous nature but the fact that state values are used by functions based on their current closures, and state updates will reflect in the next re-render by which the existing closures are not affected, but new ones are created. Now in the current state, the values within hooks are obtained by existing closures, and when a re-render happens, the closures are updated based on whether the function is recreated again or not.
Even if you add a setTimeout the function, though the timeout will run after some time by which the re-render would have happened, the setTimeout will still use the value from its previous closure and not the updated one.
setMovies(result);
console.log(movies) // movies here will not be updated
If you want to perform an action on state update, you need to use the useEffect hook, much like using componentDidUpdate in class components since the setter returned by useState doesn't have a callback pattern
useEffect(() => {
// action on update of movies
}, [movies]);
As far as the syntax to update state is concerned, setMovies(result) will replace the previous movies value in the state with those available from the async request.
However, if you want to merge the response with the previously existing values, you must use the callback syntax of state updation along with the correct use of spread syntax like
setMovies(prevMovies => ([...prevMovies, ...result]));
Additional details to the previous answer:
While React's setState is asynchronous (both classes and hooks), and it's tempting to use that fact to explain the observed behavior, it is not the reason why it happens.
TLDR: The reason is a closure scope around an immutable const value.
Solutions:
read the value in render function (not inside nested functions):
useEffect(() => { setMovies(result) }, [])
console.log(movies)
add the variable into dependencies (and use the react-hooks/exhaustive-deps eslint rule):
useEffect(() => { setMovies(result) }, [])
useEffect(() => { console.log(movies) }, [movies])
use a temporary variable:
useEffect(() => {
const newMovies = result
console.log(newMovies)
setMovies(newMovies)
}, [])
use a mutable reference (if we don't need a state and only want to remember the value - updating a ref doesn't trigger re-render):
const moviesRef = useRef(initialValue)
useEffect(() => {
moviesRef.current = result
console.log(moviesRef.current)
}, [])
Explanation why it happens:
If async was the only reason, it would be possible to await setState().
However, both props and state are assumed to be unchanging during 1 render.
Treat this.state as if it were immutable.
With hooks, this assumption is enhanced by using constant values with the const keyword:
const [state, setState] = useState('initial')
The value might be different between 2 renders, but remains a constant inside the render itself and inside any closures (functions that live longer even after render is finished, e.g. useEffect, event handlers, inside any Promise or setTimeout).
Consider following fake, but synchronous, React-like implementation:
// sync implementation:
let internalState
let renderAgain
const setState = (updateFn) => {
internalState = updateFn(internalState)
renderAgain()
}
const useState = (defaultState) => {
if (!internalState) {
internalState = defaultState
}
return [internalState, setState]
}
const render = (component, node) => {
const {html, handleClick} = component()
node.innerHTML = html
renderAgain = () => render(component, node)
return handleClick
}
// test:
const MyComponent = () => {
const [x, setX] = useState(1)
console.log('in render:', x) // ✅
const handleClick = () => {
setX(current => current + 1)
console.log('in handler/effect/Promise/setTimeout:', x) // ❌ NOT updated
}
return {
html: `<button>${x}</button>`,
handleClick
}
}
const triggerClick = render(MyComponent, document.getElementById('root'))
triggerClick()
triggerClick()
triggerClick()
<div id="root"></div>
I know that there are already very good answers. But I want to give another idea how to solve the same issue, and access the latest 'movie' state, using my module react-useStateRef.
As you understand by using React state you can render the page every time the state change. But by using React ref, you can always get the latest values.
So the module react-useStateRef let you use state's and ref's together. It's backward compatible with React.useState, so you can just replace the import statement
const { useEffect } = React
import { useState } from 'react-usestateref'
const [movies, setMovies] = useState(initialValue);
useEffect(() => {
(async function() {
try {
const result = [
{
id: "1546514491119",
},
];
console.log("result =", result);
setMovies(result);
console.log("movies =", movies.current); // will give you the latest results
} catch (e) {
console.error(e);
}
})();
}, []);
More information:
react-usestsateref
I just finished a rewrite with useReducer, following #kentcdobs article (ref below) which really gave me a solid result that suffers not one bit from these closure problems.
See: https://kentcdodds.com/blog/how-to-use-react-context-effectively
I condensed his readable boilerplate to my preferred level of DRYness -- reading his sandbox implementation will show you how it actually works.
import React from 'react'
// ref: https://kentcdodds.com/blog/how-to-use-react-context-effectively
const ApplicationDispatch = React.createContext()
const ApplicationContext = React.createContext()
function stateReducer(state, action) {
if (state.hasOwnProperty(action.type)) {
return { ...state, [action.type]: state[action.type] = action.newValue };
}
throw new Error(`Unhandled action type: ${action.type}`);
}
const initialState = {
keyCode: '',
testCode: '',
testMode: false,
phoneNumber: '',
resultCode: null,
mobileInfo: '',
configName: '',
appConfig: {},
};
function DispatchProvider({ children }) {
const [state, dispatch] = React.useReducer(stateReducer, initialState);
return (
<ApplicationDispatch.Provider value={dispatch}>
<ApplicationContext.Provider value={state}>
{children}
</ApplicationContext.Provider>
</ApplicationDispatch.Provider>
)
}
function useDispatchable(stateName) {
const context = React.useContext(ApplicationContext);
const dispatch = React.useContext(ApplicationDispatch);
return [context[stateName], newValue => dispatch({ type: stateName, newValue })];
}
function useKeyCode() { return useDispatchable('keyCode'); }
function useTestCode() { return useDispatchable('testCode'); }
function useTestMode() { return useDispatchable('testMode'); }
function usePhoneNumber() { return useDispatchable('phoneNumber'); }
function useResultCode() { return useDispatchable('resultCode'); }
function useMobileInfo() { return useDispatchable('mobileInfo'); }
function useConfigName() { return useDispatchable('configName'); }
function useAppConfig() { return useDispatchable('appConfig'); }
export {
DispatchProvider,
useKeyCode,
useTestCode,
useTestMode,
usePhoneNumber,
useResultCode,
useMobileInfo,
useConfigName,
useAppConfig,
}
With a usage similar to this:
import { useHistory } from "react-router-dom";
// https://react-bootstrap.github.io/components/alerts
import { Container, Row } from 'react-bootstrap';
import { useAppConfig, useKeyCode, usePhoneNumber } from '../../ApplicationDispatchProvider';
import { ControlSet } from '../../components/control-set';
import { keypadClass } from '../../utils/style-utils';
import { MaskedEntry } from '../../components/masked-entry';
import { Messaging } from '../../components/messaging';
import { SimpleKeypad, HandleKeyPress, ALT_ID } from '../../components/simple-keypad';
export const AltIdPage = () => {
const history = useHistory();
const [keyCode, setKeyCode] = useKeyCode();
const [phoneNumber, setPhoneNumber] = usePhoneNumber();
const [appConfig, setAppConfig] = useAppConfig();
const keyPressed = btn => {
const maxLen = appConfig.phoneNumberEntry.entryLen;
const newValue = HandleKeyPress(btn, phoneNumber).slice(0, maxLen);
setPhoneNumber(newValue);
}
const doSubmit = () => {
history.push('s');
}
const disableBtns = phoneNumber.length < appConfig.phoneNumberEntry.entryLen;
return (
<Container fluid className="text-center">
<Row>
<Messaging {...{ msgColors: appConfig.pageColors, msgLines: appConfig.entryMsgs.altIdMsgs }} />
</Row>
<Row>
<MaskedEntry {...{ ...appConfig.phoneNumberEntry, entryColors: appConfig.pageColors, entryLine: phoneNumber }} />
</Row>
<Row>
<SimpleKeypad {...{ keyboardName: ALT_ID, themeName: appConfig.keyTheme, keyPressed, styleClass: keypadClass }} />
</Row>
<Row>
<ControlSet {...{ btnColors: appConfig.buttonColors, disabled: disableBtns, btns: [{ text: 'Submit', click: doSubmit }] }} />
</Row>
</Container>
);
};
AltIdPage.propTypes = {};
Now everything persists smoothly everywhere across all my pages
React's useEffect has its own state/lifecycle. It's related to mutation of state, and it will not update the state until the effect is destroyed.
Just pass a single argument in parameters state or leave it a black array and it will work perfectly.
React.useEffect(() => {
console.log("effect");
(async () => {
try {
let result = await fetch("/query/countries");
const res = await result.json();
let result1 = await fetch("/query/projects");
const res1 = await result1.json();
let result11 = await fetch("/query/regions");
const res11 = await result11.json();
setData({
countries: res,
projects: res1,
regions: res11
});
} catch {}
})(data)
}, [setData])
# or use this
useEffect(() => {
(async () => {
try {
await Promise.all([
fetch("/query/countries").then((response) => response.json()),
fetch("/query/projects").then((response) => response.json()),
fetch("/query/regions").then((response) => response.json())
]).then(([country, project, region]) => {
// console.log(country, project, region);
setData({
countries: country,
projects: project,
regions: region
});
})
} catch {
console.log("data fetch error")
}
})()
}, [setData]);
Alternatively, you can try React.useRef() for instant change in the React hook.
const movies = React.useRef(null);
useEffect(() => {
movies.current='values';
console.log(movies.current)
}, [])
The closure is not the only reason.
Based on the source code of useState (simplified below). Seems to me the value is never assigned right away.
What happens is that an update action is queued when you invoke setValue. And after the schedule kicks in and only when you get to the next render, these update action then is applied to that state.
Which means even we don't have closure issue, react version of useState is not going to give you the new value right away. The new value doesn't even exist until next render.
function useState(initialState) {
let hook;
...
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = action(baseState); // setValue HERE
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending);
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}
function dispatchAction(queue, action) {
const update = {
action,
next: null
};
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
isMount = false;
workInProgressHook = fiber.memoizedState;
schedule();
}
There's also an article explaining the above in the similar way, https://dev.to/adamklein/we-don-t-know-how-react-state-hook-works-1lp8
I too was stuck with the same problem. As other answers above have clarified the error here, which is that useState is asynchronous and you are trying to use the value just after setState. It is not updating on the console.log() part because of the asynchronous nature of setState, it lets your further code to execute, while the value updating happens on the background. Thus you are getting the previous value. When the setState is completed on the background it will update the value and you will have access to that value on the next render.
If anyone is interested to understand this in detail. Here is a really good Conference talk on the topic.
https://www.youtube.com/watch?v=8aGhZQkoFbQ
I found this to be good. Instead of defining state (approach 1) as, example,
const initialValue = 1;
const [state,setState] = useState(initialValue)
Try this approach (approach 2),
const [state = initialValue,setState] = useState()
This resolved the rerender issue without using useEffect since we are not concerned with its internal closure approach with this case.
P.S.: If you are concerned with using old state for any use case then useState with useEffect needs to be used since it will need to have that state, so approach 1 shall be used in this situation.
If we have to update state only, then a better way can be if we use the push method to do so.
Here is my code. I want to store URLs from Firebase in state.
const [imageUrl, setImageUrl] = useState([]);
const [reload, setReload] = useState(0);
useEffect(() => {
if (reload === 4) {
downloadUrl1();
}
}, [reload]);
const downloadUrl = async () => {
setImages([]);
try {
for (let i = 0; i < images.length; i++) {
let url = await storage().ref(urls[i].path).getDownloadURL();
imageUrl.push(url);
setImageUrl([...imageUrl]);
console.log(url, 'check', urls.length, 'length', imageUrl.length);
}
}
catch (e) {
console.log(e);
}
};
const handleSubmit = async () => {
setReload(4);
await downloadUrl();
console.log(imageUrl);
console.log('post submitted');
};
This code works to put URLs in state as an array. This might also work for you.
With custom hooks from my library, you can wait for the state values to update:
useAsyncWatcher(...values):watcherFn(peekPrevValue: boolean)=>Promise - is a promise wrapper around useEffect that can wait for updates and return a new value and possibly a previous one if the optional peekPrevValue argument is set to true.
(Live Demo)
import React, { useState, useEffect, useCallback } from "react";
import { useAsyncWatcher } from "use-async-effect2";
function TestComponent(props) {
const [counter, setCounter] = useState(0);
const [text, setText] = useState("");
const textWatcher = useAsyncWatcher(text);
useEffect(() => {
setText(`Counter: ${counter}`);
}, [counter]);
const inc = useCallback(() => {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
setCounter((counter) => counter + 1);
const updatedText = await textWatcher();
console.log(updatedText);
})();
}, []);
return (
<div className="component">
<div className="caption">useAsyncEffect demo</div>
<div>{counter}</div>
<button onClick={inc}>Inc counter</button>
</div>
);
}
export default TestComponent;
useAsyncDeepState is a deep state implementation (similar to this.setState (patchObject)) whose setter can return a promise synchronized with the internal effect. If the setter is called with no arguments, it does not change the state values, but simply subscribes to state updates. In this case, you can get the state value from anywhere inside your component, since function closures are no longer a hindrance.
(Live Demo)
import React, { useCallback, useEffect } from "react";
import { useAsyncDeepState } from "use-async-effect2";
function TestComponent(props) {
const [state, setState] = useAsyncDeepState({
counter: 0,
computedCounter: 0
});
useEffect(() => {
setState(({ counter }) => ({
computedCounter: counter * 2
}));
}, [state.counter]);
const inc = useCallback(() => {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
await setState(({ counter }) => ({ counter: counter + 1 }));
console.log("computedCounter=", state.computedCounter);
})();
});
return (
<div className="component">
<div className="caption">useAsyncDeepState demo</div>
<div>state.counter : {state.counter}</div>
<div>state.computedCounter : {state.computedCounter}</div>
<button onClick={() => inc()}>Inc counter</button>
</div>
);
}
var [state,setState]=useState(defaultValue)
useEffect(()=>{
var updatedState
setState(currentState=>{ // Do not change the state by get the updated state
updateState=currentState
return currentState
})
alert(updateState) // the current state.
})
Without any addtional NPM package
//...
const BackendPageListing = () => {
const [ myData, setMyData] = useState( {
id: 1,
content: "abc"
})
const myFunction = ( x ) => {
setPagenateInfo({
...myData,
content: x
})
console.log(myData) // not reflecting change immediately
let myDataNew = {...myData, content: x };
console.log(myDataNew) // Reflecting change immediately
}
return (
<>
<button onClick={()=>{ myFunction("New Content")} }>Update MyData</button>
</>
)
Not saying to do this, but it isn't hard to do what the OP asked without useEffect.
Use a promise to resolve the new state in the body of the setter function:
const getState = <T>(
setState: React.Dispatch<React.SetStateAction<T>>
): Promise<T> => {
return new Promise((resolve) => {
setState((currentState: T) => {
resolve(currentState);
return currentState;
});
});
};
And this is how you use it (example shows the comparison between count and outOfSyncCount/syncCount in the UI rendering):
const App: React.FC = () => {
const [count, setCount] = useState(0);
const [outOfSyncCount, setOutOfSyncCount] = useState(0);
const [syncCount, setSyncCount] = useState(0);
const handleOnClick = async () => {
setCount(count + 1);
// Doesn't work
setOutOfSyncCount(count);
// Works
const newCount = await getState(setCount);
setSyncCount(newCount);
};
return (
<>
<h2>Count = {count}</h2>
<h2>Synced count = {syncCount}</h2>
<h2>Out of sync count = {outOfSyncCount}</h2>
<button onClick={handleOnClick}>Increment</button>
</>
);
};
Use the Background Timer library. It solved my problem.
const timeoutId = BackgroundTimer.setTimeout(() => {
// This will be executed once after 1 seconds
// even when the application is the background
console.log('tac');
}, 1000);
// replace
return <p>hello</p>;
// with
return <p>{JSON.stringify(movies)}</p>;
Now you should see, that your code actually does work. What does not work is the console.log(movies). This is because movies points to the old state. If you move your console.log(movies) outside of useEffect, right above the return, you will see the updated movies object.

Vue3 Composition API emit works without store but not with store

I am using Vue3 with the composition API. I have set up a store (not using VUEX) to store global settings. I have a sidebar that I want to expand or collapse. I first put all of the code in one component to emit to the parent to expand or collapse a sidebar, works great. I then tried to refactor to use the store to have the sidebar set across a views but the emit will not work in the second version.
Without using the store, all code is local to the component. It works great and emits the value to the parent.
//regular
import { ref } from "vue";
export default {
name: "rightmenu",
emits: ["showRightMenu"],
props: ["bgcolor"],
setup(props, context) {
const showRightMenu = ref(true);
function toggleRightMenu() {
if (showRightMenu.value == false) {
showRightMenu.value = true;
} else {
showRightMenu.value = false;
}
context.emit("showRightMenu", showRightMenu.value);
}
return { showRightMenu, toggleRightMenu };
},
};
The following uses the store to update the item and run methods.
//with store
import { ref, inject } from "vue";
export default {
name: "rightmenu",
emits: ["showRightMenu"],
props: ["bgcolor"],
setup(context) {
const store = inject("store");
const showRightMenu = ref(store.showRightMenu);
function toggleRightMenu() {
const aValue = store.methods.toggleRightMenu();
context.emit("showRightMenu", aValue); //ISSUE
}
return { showRightMenu, toggleRightMenu };
},
};
I get the following error:
Uncaught TypeError: context.emit is not a function
at Proxy.toggleRightMenu
Here is the store code:
import { ref } from "vue";
const showRightMenu = ref(true);
const methods = {
changeColor(val) {
state_color.color = val.color;
state_color.colorName = val.title;
},
toggleRightMenu() {
if (showRightMenu.value == false) {
showRightMenu.value = true;
} else {
showRightMenu.value = false;
}
return showRightMenu.value
},
};
const getters = {};
export default {
showRightMenu,
methods,
getters,
};
It looks like your setup function is incorrect.
in the first example, you are using the props as first argument, but not in the second.
setup(props, context) { works
setup(context) { doesn't because the context arg is actually populated by props and not context

Passing variables from middleware to page in Next.js 12 new middleware api

Background to the Question
Vercel recently released their biggest update ever to Next.js. Next.js blog.
They introduced a lot of new features but my favorite is Middleware which:
"enables you to use code over configuration. This gives you full
flexibility in Next.js because you can run code before a request is
completed. Based on the user's incoming request, you can modify the
response by rewriting, redirecting, adding headers, or even streaming
HTML."
The Question
The following structure is used in this question.
- /pages
index.js
signin.js
- /app
_middleware.js # Will run before everything inside /app folder
index.js
The two important files here are /app/_middleware.js and /app/index.js.
// /app/_middleware.js
import { NextResponse } from 'next/server';
export function middleware(req, event) {
const res = { isSignedIn: true, session: { firstName: 'something', lastName: 'else' } }; // This "simulates" a response from an auth provider
if (res.isSignedIn) {
// Continue to /app/index.js
return NextResponse.next();
} else {
// Redirect user
return NextResponse.redirect('/signin');
}
}
// /app/index.js
export default function Home() {
return (
<div>
<h1>Authenticated!</h1>
// session.firstName needs to be passed to this file from middleware
<p>Hello, { session.firstName }</p>
</div>
);
}
In this example /app/index.js needs access to the res.session JSON data. Is it possible to pass it in the NextResponse.next() function or do you need to do something else?
In express you can do res.locals.session = res.session
According to the examples (look specifically at /pages/_middleware.ts and /lib/auth.ts) it looks like the canonical way to do this would be to set your authentication via a cookie.
In your middleware function, that would look like:
// /app/_middleware.js
import { NextResponse } from 'next/server';
export function middleware(req, event) {
const res = { isSignedIn: true, session: { firstName: 'something', lastName: 'else' } }; // This "simulates" a response from an auth provider
if (res.isSignedIn) {
// Continue to /app/index.js
return NextResponse.next().cookie("cookie_key", "cookie_value"); // <--- SET COOKIE
} else {
// Redirect user
return NextResponse.redirect('/signin');
}
}
There's a another way but just like using cookie to achieve this. Just pass you data through headers.
// middleware.ts
async function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('X-HEADER', 'some-value-to-pass');
return response;
}
// _app.ts
function MyApp({ data }) {
// you can access your data here
<div>{data}</div>
}
MyApp.getInitialProps = ({ ctx }) => {
const data = ctx.res.getHeader('X-HEADER');
ctx.res.removeHeader('X-HEADER');
return { data };
};
Only weird solution is to inject your custom object into req.body because next.js v12 middleware doesn't allow altering the NextApiRequest
export const middleware = async (req: NextApiRequest) => {
// return new Response("Hello, world!");
req.body = { ...req.body, foo: "bar" };
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
await middleware(req);
// now req.body.foo=='bar'
}
They do however explain how you can extend middleware here, but the example given (copied below) isn't meaningful enough because it doesnt show how withFoo() is implemented
import { NextApiRequest, NextApiResponse } from 'next'
import { withFoo } from 'external-lib-foo'
type NextApiRequestWithFoo = NextApiRequest & {
foo: (bar: string) => void
}
const handler = (req: NextApiRequestWithFoo, res: NextApiResponse) => {
req.foo('bar') // we can now use `req.foo` without type errors
res.end('ok')
}
export default withFoo(handler)
I assumed based on the above, withFoo.ts should be like this. But still wasn't successful in accessing request.Foo()
import { NextApiHandler, NextApiRequest } from "next";
export const withFoo = (handler: NextApiHandler) => {
//do stuff
};
Maybe someone can chip in?
We found a solution for 12.2+ middleware - published here:
https://clerk.dev/blog/nextjs-pass-value-from-middleware-to-api-routes-and-getserversideprops
And copying here for posterity...
Usage: middleware.js
import { NextResponse } from "next/server";
import { withContext } from "./context";
// Pre-define the possible context keys to prevent spoofing
const allowedContextKeys = ["foo"];
export default withContext(allowedContextKeys, (setContext, req) => {
setContext("foo", "bar");
return NextResponse.next();
});
Usage: API route (Node)
import { getContext } from "../../context";
export default function handler(req, res) {
res.status(200).json({ foo: getContext(req, "foo") });
}
Usage: API route (Edge)
import { getContext } from "../../context";
export default function handler(req) {
return new Response(JSON.stringify({ foo: getContext(req, "foo") }));
}
Usage: getServerSideProps (Edge and Node)
import { getContext } from "../context";
export const getServerSideProps = ({ req }) => {
return { props: { foo: getContext(req, "foo") } };
};
Source: (saved to context.js on your root)
import { NextResponse } from "next/server";
const ctxKey = (key) => `ctx-${key.toLowerCase()}`;
export const getContext = (req, rawKey) => {
const key = ctxKey(rawKey);
let headerValue =
typeof req.headers.get === "function"
? req.headers.get(key) // Edge
: req.headers[key]; // Node;
// Necessary for node in development environment
if (!headerValue) {
headerValue = req.socket?._httpMessage?.getHeader(key);
}
if (headerValue) {
return headerValue;
}
// Use a dummy url because some environments only return
// a path, not the full url
const reqURL = new URL(req.url, "http://dummy.url");
return reqURL.searchParams.get(key);
};
export const withContext = (allowedKeys, middleware) => {
// Normalize allowed keys
for (let i = 0; i < allowedKeys.length; i++) {
if (typeof allowedKeys[i] !== "string") {
throw new Error("All keys must be strings");
}
allowedKeys[i] = ctxKey(allowedKeys[i]);
}
return (req, evt) => {
const reqURL = new URL(req.url);
// First, make sure allowedKeys aren't being spoofed.
// Reliably overriding spoofed keys is a tricky problem and
// different hosts may behave different behavior - it's best
// just to safelist "allowedKeys" and block if they're being
// spoofed
for (const allowedKey of allowedKeys) {
if (req.headers.get(allowedKey) || reqURL.searchParams.get(allowedKey)) {
throw new Error(
`Key ${allowedKey.substring(
4
)} is being spoofed. Blocking this request.`
);
}
}
const data = {};
const setContext = (rawKey, value) => {
const key = ctxKey(rawKey);
if (!allowedKeys.includes(key)) {
throw new Error(
`Key ${rawKey} is not allowed. Add it to withContext's first argument.`
);
}
if (typeof value !== "string") {
throw new Error(
`Value for ${rawKey} must be a string, received ${typeof value}`
);
}
data[key] = value;
};
let res = middleware(setContext, req, evt) || NextResponse.next();
// setContext wasn't called, passthrough
if (Object.keys(data).length === 0) {
return res;
}
// Don't modify redirects
if (res.headers.get("Location")) {
return res;
}
const rewriteURL = new URL(
res.headers.get("x-middleware-rewrite") || req.url
);
// Don't modify cross-origin rewrites
if (reqURL.origin !== rewriteURL.origin) {
return res;
}
// Set context directly on the res object (headers)
// and on the rewrite url (query string)
for (const key in data) {
res.headers.set(key, data[key]);
rewriteURL.searchParams.set(key, data[key]);
}
// set the updated rewrite url
res.headers.set("x-middleware-rewrite", rewriteURL.href);
return res;
};
};

nextJS SSR useRouter() does not work when refresh page

I am using nextJS SSR in my project. Now when I try to use the following code to get page parameters then it shows undefined.
function About() {
const router = useRouter();
const { plan_id } = router.query;
console.log(plan_id)
}
export default About;
It works when the page is routed from some other page (without page reload with "next/link") but it does not work when I refresh the page. Can someone please help?
I found the answer self. Actually when you refresh the page then the router does not get initialized instantly. So you can add that under UseEffect hook as following and you will be able to get the parameters
function About() {
const [param1, setParam1]=useState("");
const router = useRouter();
useEffect(() => {
if (router && router.query) {
console.log(router.query);
setParam1(router.query.param1);
}
}, [router]);
}
When this router parameter will change then it will call the "UseEffect" which can be used to retrieve the values.
function About({plan_id}) {
console.log(plan_id)
}
// this function only runs on the server by Next.js
export const getServerSideProps = async ({params}) => {
const plan_id = params.plan_id;
return {
props: { plan_id }
}
}
export default About;
You can find more intel in the docs.
I fix this problem with this method.
First add getServerSideProps to your page
//MyPage.js
export async function getServerSideProps({req, query}) {
return {
props: {
initQuery: query
}
}
}
Then created useQuery function like this
//useQuery.js
export let firstQuery = {}
export default function useQuery({slugKey = 'slug', initial = {}} = {}) {
const {query = (initial || firstQuery)} = useRouter()
useEffect(() => {
if (_.isEmpty(initial) || !_.isObject(initial))
return
firstQuery = initial
}, [initial])
return useMemo(() => {
if (!_.isEmpty(query)) {
return query
}
try {
const qs = window.location.search.split('+').join(' ');
const href = window.location.href
const slug = href.substring(href.lastIndexOf('/') + 1).replace(/\?.*/gi, '')
let params = {},
tokens,
re = /[?&]?([^=]+)=([^&]*)/g;
if (slug)
params[slugKey] = slug
while (tokens = re.exec(qs)) {
params[decodeURIComponent(tokens[1])] = decodeURIComponent(tokens[2]);
}
return params
} catch {
}
}, [query])
}
And always use useQuery for receive query params
//MyPage.js
export default function MyPage({initQuery}) {
const query = useQuery({initial: initQuery})
return(
<div>
{query.myParam}
</div>
)
}
And in components like this
//MyComponent.js
export default function MyComponent() {
const query = useQuery()
return(
<div>
{query.myParam}
</div>
)
}
For those still having issues with this. Here is a solution that worked for me
function About() {
const [param1, setParam1]=useState("");
const router = useRouter();
const { param1 } = router.query()
useEffect(() => {
if (!param1) {
return;
}
// use param1
}, [param1]);
}
You can find the solution here

How to get previous URL in Next.js?

How can I get the previous URL in Next.js?
I thought the values this.props.router.asPath and nextProps.router.asPath are different.
Actually, I want to call router.push after login. I know that router.back goes to the previous page. But it's possible to go to another site. The users having history stacks go to the previous page, the users not having history stacks go to / main page.
import { Component } from 'react'
import App, { Container } from 'next/app';
import ErrorComponent from '#/components/error'
export default class MyApp extends App {
render() {
console.log(this.props)
const { Component, pageProps, router } = this.props;
const props = {
...pageProps,
router
}
return (
<ErrorBoundary>
<Container>
<Component {...props} />
</Container>
</ErrorBoundary>
);
}
componentWillReceiveProps(nextProps) {
// previous page url /contents
console.log(this.props.router.asPath) // /about
console.log(nextProps.router.asPath) // /about
console.log('window.history.previous.href', window.history.previous) // undefined
}
}
How can I fix it? Or how can I get the previous URL to move page after login?
You find the Referer ( so the previous URL ) in the context of getServerSideProps or any other Data fetching methods
as
context.req.headers.referer
example in code
export async function getServerSideProps(context) {
console.log(context.req.headers.referer)
}
I've used Context do to this
In _app.tsx
import { HistoryProvider } from '../contexts/History'
const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => {
return (
<ThemeProvider theme={theme}>
<Header />
<HistoryProvider>
<Component {...pageProps} />
</HistoryProvider>...
/contexts/History.tsx
import { useRouter } from 'next/router'
import React, { createContext, useState, useEffect, useContext } from 'react'
interface HValidation {
history: string[]
setHistory(data: string[]): void
back(): void
}
const HistoryContext = createContext<HValidation>({} as HValidation)
export const HistoryProvider: React.FC = ({ children }) => {
const { asPath, push, pathname } = useRouter()
const [history, setHistory] = useState<string[]>([])
function back() {
for (let i = history.length - 2; i >= 0; i--) {
const route = history[i]
if (!route.includes('#') && route !== pathname) {
push(route)
// if you want to pop history on back
const newHistory = history.slice(0, i)
setHistory(newHistory)
break
}
}
}
useEffect(() => {
setHistory(previous => [...previous, asPath])
}, [asPath])
return (
<HistoryContext.Provider
value={{
back,
history,
setHistory,
}}
>
{children}
</HistoryContext.Provider>
)
}
export function useHistory(): HValidation {
const context = useContext(HistoryContext)
return context
}
In any component, you can use
import { useHistory } from '../../contexts/History'
const ContentHeader: React.FC<ContentHeaderProps> = ({ title, hideBack }) => {
const { history, back } = useHistory() ...
I've used this component to back history ignoring links with hash (#), because the native router.back() was bugging when i have <a href="#someid" /> to scroll page to some page ids
I wanted to go back to last page, and not the last anchor
EDIT 01/04/2021
You can also set a fallback route for "back".
back(fallbackRoute?: string): void
function back(fallbackRoute?: string) {
for (let i = history.length - 2; i >= 0; i--) {
const route = history[i]
console.log({ route, pathname })
if (!route.includes('#') && route !== pathname) {
push(route)
const newHistory = history.slice(0, i)
setHistory(newHistory)
return
}
}
if (fallbackRoute) {
router.push(fallbackRoute)
}
}
I think you can implement a custom history in global state
Something like this
_app.js
import React from 'react';
import App, { Container } from 'next/app';
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps };
}
state = {
history: [] // keep history items in state
};
componentDidMount() {
const { asPath } = this.props.router;
// lets add initial route to `history`
this.setState(prevState => ({ history: [...prevState.history, asPath] }));
}
componentDidUpdate() {
const { history } = this.state;
const { asPath } = this.props.router;
// if current route (`asPath`) does not equal
// the latest item in the history,
// it is changed so lets save it
if (history[history.length - 1] !== asPath) {
this.setState(prevState => ({ history: [...prevState.history, asPath] }));
}
}
render() {
const { Component, pageProps } = this.props;
return (
<Container>
<Component history={this.state.history} {...pageProps} />
</Container>
);
}
}
export default MyApp;
so then in your components you can navigate wherever you want within history
if (!history || !history.length) {
router.push('/');
} else {
router.push(history[history.length - 1]);
}
Hope this helps!
I was looking for a very simple way to do this since some of the answers here seem a bit complex for implementing something this simple. router.back() doesn't seem to work well in this scenario as, in my case, it'd go all the way back and out of my site some times.
So, I thought 🤔, what better way to do this than localStorage?
when I need to send the user to the '/login' route, I add the current route to localStorage
if (!auth.user) {
window.localStorage.setItem("path", router.asPath);
router.replace("/login");
return <div> redirecting to login... </div>;
}
and once the user sign-in, I send them back to the previous page (the route of which has been saved in the localStorage
if (auth.user) {
router.replace(localStorage.getItem("path") || "/");
return <div> Loading... </div>
);
}
You can observe the localStorage while testing to see what is going on. I hope this helps someone.
Let's say there's a /profile page which should be rendered iff user is logged in or else user should be redirected to /login, after user login on /login, it should be pushed to previous page (here/profile) but not on another website or New Tab.
In /profile this is how you should redirect to /login
Router.push('/login?referer=profile', '/login')
In /login after user is successfully logged in, use:
Router.push(Router.query.referer?.toString||'/')
Hope this helped.
I recently had this problem and used the following solution to route back to the previous page.
In my component I used the useRouter() hook from Next.js. This hook produces a router object which has the back() function. This function can be used on an <a> tag to redirect back in the following way.
const Component: React.FC = () => {
const router = useRouter();
return (
<>
<a onClick={() => router.back()}>Go back to the last page</a>
</>
);
};
Note that this function does not produce a URL that you can use as a value in the href, which is unfortunate. But I think this solution is simple yet effective.
Reference: https://nextjs.org/docs/api-reference/next/router#routerback
Simple Hook
Add this hook, and call it in your _app.tsx or where needed. You can compare it to router.pathname if you need to know what the change was.
const usePreviousRoute = () => {
const { asPath } = useRouter();
const ref = useRef<string | null>(null);
useEffect(() => {
ref.current = asPath;
}, [asPath]);
return ref.current;
};
This doesn't leave data behind once the page is closed (storage, cookies) and cleanly resets to null on next visit to the site.
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
// in _app.js
function MyApp({ Component, pageProps }) {
const router = useRouter();
const [history, setHistory] = useState({ previous: null, current: router.asPath });
useEffect(() => {
setHistory((oldHistory) => ({ ...oldHistory, previous: oldHistory.current, current: router.asPath }));
}, [router.asPath]);
return (<Component {...{ ...pageProps, history }} />);
}
export default MyApp;
// in a page
function MyPage({ ...pageProps }) {
return (<span>Previous route: {pageProps.history?.previous || "/"}</span>);
}
export default MyPage;
I tried doing similar to iurii's answer. My _app.js looks like this (I was trying to integrate with segment.com so felt this need)
export default class MyApp extends App {
componentDidMount () {
const { asPath } = this.props.router;
this.setState(prevState => ({ history: [...prevState.history, asPath] }));
const isBrowser = typeof window !== 'undefined';
if(isBrowser) {
// For the first page load
console.log("Going to log first load --> referrer : ", document.referrer);
// this can get me the document.referrer properly, if I come to the website from a third party source like google search.
global.analytics.page(window.location.href,
{referrer: document.referrer}
)
}
}
static async getInitialProps ({ Component, router, ctx }) {
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps }
}
state = {
history: [] // keep history items in state
};
componentDidUpdate() {
const { history } = this.state;
const { asPath } = this.props.router;
// if current route (`asPath`) does not equal
// the latest item in the history,
// it is changed so lets save it
if (history[history.length - 1] !== asPath) {
global.analytics.page(window.location.href, {
referrer: history[history.length - 1] ? history[history.length - 1] : ""
})
// this simulates the document.referrer on pages after the user navigates
this.setState(prevState => ({ history: [...prevState.history, asPath] }));
}
}
So with a combination of history[history.length - 1] ? history[history.length - 1] : "" and const isBrowser = typeof window !== 'undefined'; I am able to simulate document.referrer for all cases. But I am missing one case, suppose , I am at google, my site landing page is A, then A points to B
Then google to A --> I get document.referrer as google
Then A to B --> I get document.referrer as A which is consistent with the behavior.
But Now If I refresh page B, then my document.referrer becomes google again.
I think I can save the last known previous URL in local storage , but that will be a anti-pattern as the browser back button can correctly take the user to the previous page (A), so somewhere the data is there already. Currently I can live with this solution as I only use this for analytics purpose on segment.com and google analytics, so refreshing will mess up my analytics numbers slightly, but still looking forward to a perfect solution so get exact data.
I can not get previous URL but with the code below I can find have a back URL or no:
typeof window !== 'undefined' && +window?.history?.state?.idx > 0
const back = async () => {
if (typeof window !== 'undefined' && +window?.history?.state?.idx > 0) {
await Router.back()
} else {
await Router.replace(fallbackURL)
}
}
A more simple way is
import { useRouter } from 'next/router';
const router = useRouter();
router.back()

Resources