Next.js router: how to make page refresh on url change? - javascript

I have page where a user can narrow their search using filters. The issue is that when a user clicks on a filter to filter properties by "rent" or "buy", the url does not refresh to reflect the changes. The changes do however appear in the URL, which is what I want, but I have to manually press enter to refresh the page with the specified filters, so that changes would appear.
As you can see in the photo, the properties listed are not "for rent" properties, so the only way to correctly see the rental properties is to manually enter the url: http://localhost:3000/search?purpose=for-rent&minPrice=20000
import {
Flex,
Select,
Box,
} from "#chakra-ui/react";
import { useRouter } from "next/router";
import Image from "next/image";
import { filterData, getFilterValues } from "../utils/filterData";
class searchFilters extends Component {
constructor(props) {
super(props);
this.state = {
filters: filterData,
};
}
handleChange = (filterValues) => {
const path = this.props.params.pathname;
const { query } = this.props.params;
const values = getFilterValues(filterValues);
values.forEach((item) => {
if (item.value && filterValues?.[item.name]) {
query[item.name] = item.value;
}
});
this.props.params.push({ pathname: path, query: query });
};
render() {
const { filters } = this.state;
return (
<Flex bg="gray.100" p="4" justifyContent="center" flexWrap="wrap">
{filters.map((filter) => (
<Box key={filter.queryName}>
<Select
placeholder={filter.placeholder}
w="fit-content"
p="2"
onChange={(e) =>
this.handleChange({ [filter.queryName]: e.target.value })
}
>
{filter.items.map((item) => (
<option value={item.value} key={item.value}>
{item.name}
</option>
))}
</Select>
</Box>
))}
</Flex>
);
}
}
const withParams = (Component) => {
return (props) => <Component {...props} params={useRouter()} />;
};
export default withParams(searchFilters);

As you are using the same component, it will not reload the page. You can detect the param change with useEffect hook and add the refreshig logic within it. This would reload the data as per the new param.
const { query } = useRouter();
useEffect(() => {
// Refresh logic
}, [query.purpose)]);

Related

Trying to display one element from an Array -ReactJs

I am trying to make a flashcard web app for language learning and/or rote learning. I have managed to show the first element of the array which contains the data that I'm fetching from the backend but I can't switch from the first element to the subsequent elements.
Here is my code in React:
// Decklist component that displays the flashcard
import { React, useEffect, useState, useContext } from "react";
import Card from "./Card";
import cardContext from "../store/cardContext";
const axios = require("axios");
export default function Decklist() {
//State for data fetched from db
const [data, setData] = useState([]);
//State for array element to be displayed from the "data" state
const [position, setPosition] = useState(0);
//function to change the array element to be displayed after user reads card
const setVisibility = () => {
setPosition(position++);
};
//function to change the difficulty of a card
const difficultyHandler = (difficulty, id) => {
console.log(difficulty);
setData(
data.map((ele) => {
if (ele.ID === id) {
return { ...ele, type: difficulty };
}
return ele;
})
);
};
//useEffect for fetching data from db
useEffect(() => {
axios
.get("/api/cards")
.then((res) => {
if (res.data) {
console.log(res.data);
setData(res.data.sort(() => (Math.random() > 0.5 ? 1 : -1)));
}
})
.catch((err) => {
console.log(err);
});
}, []);
return (
<cardContext.Provider
value={{ cardData: data, setDifficulty: difficultyHandler }}
>
{data.length && (
<Card
position={position}
// dataIndex={index}
visible={setVisibility}
id={data[position].ID}
front={data[position].Front}
back={data[position].Back}
/>
)}
</cardContext.Provider>
);
}
//Card component
import { React, useState, useEffect } from "react";
import Options from "./Options";
export default function Card(props) {
//State for showing or hiding the answer
const [reverse, setReverse] = useState(false);
const [display, setDisplay] = useState(true);
//function for showing the answer
const reversalHandler = () => {
setReverse(true);
};
return (
<div>
{reverse ? (
<div className="card">
{props.front} {props.back}
<button
onClick={() => {
props.visible();
}}
>
Next Card
</button>
</div>
) : (
<div className="card">{props.front}</div>
)}
<Options
visible={props.visible}
reverse={reversalHandler}
id={props.id}
/>
</div>
);
}
//Options Component
import { React, useContext, useState } from "react";
import cardContext from "../store/cardContext";
export default function Options(props) {
const ctx = useContext(cardContext);
const [display, setDisplay] = useState(true);
return (
<>
<div className={display ? "" : "inactive"}>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("easy", props.id);
}}
>
Easy
</button>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("medium", props.id);
}}
>
Medium
</button>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("hard", props.id);
}}
>
Hard
</button>
</div>
</>
);
}
The setVisibility function in the Decklist component is working fine and setting the position state properly. However, I don't know how to re-render the Card component so that it acts on the position state that has changed.
One way to force a re-render of a component is to set its state to itself
onClick={() => {
props.visible();
setReverse(reverse);
}}
However this probably isn't your issue as components will automatically re-render when their state changes or a parent re-renders. This means that for some reason the Card component isn't actually changing the parent component.

Next.js localStorage not defined even using useEffect

I know, there is a lot of similar questions although I could not find a solution to my problem. It is the first time I am using Next.js and TypeScrypt.
I am simulating a login with REQRES storing the token in the localStorage as shown below:
import {
FormControl,
FormLabel,
Input,
Heading,
Flex,
Button,
useToast,
} from '#chakra-ui/react';
import { useRouter } from 'next/router';
import { useState } from 'react';
import LStorage from '../utils/localStorage/index';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleEmail = (e: any) => setEmail(e.target.value);
const handlePassword = (e: any) => setPassword(e.target.value);
const router = useRouter();
const toast = useToast();
const success = () => toast({
title: 'Login Successfull',
description: 'You will be redirected now.',
status: 'success',
duration: 1200,
isClosable: true,
});
const failure = (error: string) => toast({
title: 'Login unsuccessfull',
description: error,
status: 'error',
duration: 3000,
isClosable: true,
});
const login = async () => {
const res = await fetch('/api', {
method: 'POST',
body: JSON.stringify({
email,
password,
}),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
});
const json = await res.json();
console.log(json);
if (json.error) {
failure(json.error);
setEmail('');
setPassword('');
} else {
LStorage.set('userToken', json.token);
LStorage.set('userInfo', email);
success();
setTimeout(() => {
router.push('/users');
}, 1500);
}
};
return (<div>
<Flex justifyContent="center">
<Heading my="5">Login</Heading>
</Flex>
<FormControl>
<FormLabel htmlFor="email">Email:</FormLabel>
<Input id="email" type="email" onChange={handleEmail} value={email}/>
<FormLabel htmlFor="password">Password:</FormLabel>
<Input id="password" type="password" onChange={handlePassword} value={password}/>
</FormControl>
<br />
<Button onClick={login}>Login</Button>
</div>);
};
export default Login;
which seem to work fine. Although when trying to get the userInfo from localStorage at the _app.tsx component I get the localStorage not defined, looking for the error I found out the solution below inside the useEffect.
import '../styles/globals.sass';
import { ChakraProvider } from '#chakra-ui/react';
import type { AppProps } from 'next/app';
import { useState, useEffect } from 'react';
import NavBar from '../components/NavBar';
import MainLayout from '../layouts/mainLayout';
import theme from '../styles/theme';
import LStorage from '../utils/localStorage/index';
function MyApp({ Component, pageProps }: AppProps) {
const [userInfo, setUserInfo] = useState<string | null>(null);
const logout = () => {
LStorage.remove('userToken');
LStorage.remove('userInfo');
setUserInfo(null);
};
useEffect(() => {
if (typeof window !== 'undefined') {
if (LStorage.get('userInfo')) {
setUserInfo(LStorage.get('userInfo'));
}
}
console.log('i am here');
}, []);
return (
<ChakraProvider theme={theme}>
<NavBar user={userInfo} logout={logout} />
<MainLayout>
<Component {...pageProps}/>
</MainLayout>
</ChakraProvider>
);
}
export default MyApp;
I understood that the first run will be on the server-side and that is why I got the error, nevertheless, using the useEffect should fix it. The thing is the useEffect does not even run unless I refresh the page... What am I missing??!??
The Login.js is a page inside page folder and the NavBar is a component inside components folder in the root.
import {
Flex, Spacer, Box, Heading, Button,
} from '#chakra-ui/react';
import Link from 'next/link';
import { FC } from 'react';
interface NavBarProps {
user: string | null;
logout: () => void;
}
const NavBar: FC<NavBarProps> = ({ user, logout }: NavBarProps) => (
<Flex bg="black" color="white" p="4">
<Box p="2">
<Heading size="md">
<Link href="/">My Sanjow App</Link>
</Heading>
</Box>
<Spacer />
{user && (
<Box pt="2" pr="4">
<Heading size="md">
<Link href="/users">Users</Link>
</Heading>
</Box>
)}
{user ? (
<Button
variant="ghost"
pr="4"
onClick={logout}
>
<Heading size="md">
<Link href="/">Logout</Link>
</Heading>
</Button>
) : (
<Box pt="2" pr="4">
<Heading size="md">
<Link href="/login">Login</Link>
</Heading>
</Box>
)}
</Flex>
);
export default NavBar;
The utils/localStorage/index
const lsType = {
set: 'setItem',
get: 'getItem',
remove: 'removeItem',
};
const ls = (type: string, itemName: string, itemData?: string): void | string => {
if (typeof window !== 'undefined') {
// eslint-disable-next-line no-undef
const LS = window.localStorage;
if (type === lsType.set && itemData) {
LS[type](itemName, itemData);
return;
}
return LS[type](itemName);
}
};
export default {
set(itemName: string, itemData: string): void {
ls(lsType.set, itemName, itemData);
},
get(itemName: string): string {
return ls(lsType.get, itemName) as string;
},
remove(itemName: string): void {
ls(lsType.remove, itemName);
},
};
You are running the effect only once by passing the [] empty array, pass the props that you expect to change instead of a blank array.
via the docs:
If you want to run an effect and clean it up only once (on mount and >unmount), you can pass an empty array ([]) as a second argument. This tells >React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.
Generally speaking, managing userInfo only via localStorage is not a good idea, since you might want to re-render the application when user logs in or logs out, or any other change to the user data (i.e. change the username), and React is not subscribed to changes done to localStorage.
Instead, React has an instrument for runtime data management like that, it's called React Context. That context (let's call it UserContext) could be initializing from localStorage, so that the case when you refresh the page for example. But after that initial bootstrapping all state management should go thru the context. Just don't forget to update both context and localStorage every time you login/logout.
I hope this is just enough to give you the right direction.

Pass a function from a component to another (ReactJS)

I'm building an application focused on showing a user's github repositories and information. In a "Section" component I fetch these repositories and display them on the screen.
In the other component "Menu" I wanted it to count these repositories and display them. Should I use props in this case?
Section Component
import React, { useState } from 'react'
import axios from 'axios'
import { Square, Wrapper, Input, Button } from './Section.styled'
export default function Section() {
const [username, setUsername] = useState("");
const [loading, setLoading] = useState(false);
const [repos, setRepos] = useState([]);
const searchRepos = () => {
setLoading(true);
axios({
method: "get",
url: `https://api.github.com/users/${username}/repos`,
}).then(res => {
setLoading(false);
setRepos(res.data);
})
}
const handleSubmit = (e) => {
e.preventDefault();
searchRepos()
}
const renderRepo = (repo)=>{
return(
<Square>
{repo.name}
</Square>
)
}
return (
<>
<Wrapper>
<Input
placeholder="Usuário"
value={username}
onChange={e => { setUsername(e.target.value) }}
/>
<Button
onClick={handleSubmit}
type="submit">
{loading ? "Buscando..." : "Buscar"}
</Button>
{repos.map(renderRepo)}
</Wrapper>
</>
)
}
Menu Component
import React from "react";
import { bool } from "prop-types";
import { StyledMenu } from "./Menu.styled";
const Menu = ({ open, ...props }) => {
const isHidden = open ? true : false;
const tabIndex = isHidden ? 0 : -1;
return (
<>
<StyledMenu open={open} aria-hidden={!isHidden} {...props}>
<a href="/" tabIndex={tabIndex}>
Repositories:
</a>
<a href="/" tabIndex={tabIndex}>
Followeres:
</a>
<a href="/" tabIndex={tabIndex}>
Following:
</a>
</StyledMenu>
</>
);
};
Menu.propTypes = {
open: bool.isRequired,
};
export default Menu;
These solutions could be possible in this case when we have received data in one component and want it to appear in another component.
Pass the function searchRepos as a prop from the parent component of both section and menu component to section component, call the function from section, this will set data in parent, and send data to the menu component as props, i.e. called Lifting up the state.
If the components are far away (deeply nested or have unrelated parent, branch) you can simply make use of context store
3. Last way is to store the data of called API of the component section in browser local storage and use it in menu component. (NOT RECOMMENDED)

How to set value through patch event to any field from custom input in sanity.io?

I want to make patch event from custom component and set a value to another field in document, but couldn’t find documentation about patch events.
there are only example without field specification:
PatchEvent.from(set(value))
Does anybody knows how to specify field name?
This opportunity contains in documentation, but without examples
https://www.sanity.io/docs/custom-input-widgets#patch-format-0b8645cc9559
I couldn't get PatchEvent.from to work at all for other fields inside a custom input component but useDocumentOperation combined with withDocument from part:#sanity/form-builder
This is a rough working example using a custom component:
import React from "react";
import FormField from "part:#sanity/components/formfields/default";
import { withDocument } from "part:#sanity/form-builder";
import { useDocumentOperation } from "#sanity/react-hooks";
import PatchEvent, { set, unset } from "part:#sanity/form-builder/patch-event";
// tried .from(value, ["slug"]) and a million variations to upate the slug but to no avail
const createPatchFrom = (value) => {
return PatchEvent.from(value === "" ? unset() : set(value));
};
const ref = React.createRef();
const RefInput = React.forwardRef((props, ref) => {
const { onChange, document } = props;
// drafts. cause an error so remove
const {patch} = useDocumentOperation(
document._id.replace("drafts.", ""), document._type)
const setValue = (value) => {
patch.execute([{set: {slug: value.toLowerCase().replace(/\s+/g, "-")}}])
onChange(createPatchFrom(value));
// OR call patch this way
patch.execute([{set: {title: value}}])
};
return (
<input
value={document.title}
ref={ref}
onChange={(e) => setValue(e.target.value)}
/>
);
});
class CustomInput extends React.Component {
// this._input is called in HOC
focus = () => {
ref.current.focus();
};
render() {
const { title } = this.props.type;
return (
<FormField label={title}>
<RefInput ref={ref} {...this.props} />
</FormField>
);
}
}
export default withDocument(CustomInput);

React state change not showing changes when going back

I have an app that lists books on a users shelf and then on a subsequent search page.
The user goes to the search page, finds a title and selects a shelf for the title to be shelved on. When they go back to the home page this title should then show on the correct shelf.
The functionality works in that the changes are made to the objects, but when I click on the home button or back button in the browser the changes do not show until I have refreshed the browser.
What do I need to do to ensure this change is shown when the user browses to the home page?
I've put the bulk of the code into Codesandbox
App.js
import React, { Component } from 'react'
import ListBooks from './ListBooks'
import SearchBooks from './SearchBooks'
import * as BooksAPI from './utils/BooksAPI'
import { Route } from 'react-router-dom'
class BooksApp extends Component {
state = {
books: []
}
componentDidMount() {
BooksAPI.getAll()
.then((books) => {
this.setState(() => ({
books
}))
})
}
updateShelf = (book, shelf) => {
const bookFromState = this.state.books.find(b => b.id === book.id);
if (bookFromState) {
// update existing
bookFromState.shelf = shelf;
this.setState(currentState => ({
books: currentState.books
}));
} else {
// add new one
this.setState(prevState => ({
books: prevState.books
}));
}
BooksAPI.update(book, shelf);
};
render() {
return (
<div>
<Route exact path='/' render={() => (
<ListBooks
books={this.state.books}
onUpdateShelf={this.updateShelf}
/>
)} />
<Route exact path='/search' render={() => (
<SearchBooks
books={this.state.books}
onUpdateShelf={this.updateShelf}
/>
)} />
</div>
)
}
}
export default BooksApp
So I checked your code. You had problem updating your state actually. The books wasn't changing after you selected a book from within the search screen. Here's the code from your App.js:
updateShelf = (book, shelf) => {
console.log(book, shelf)
const bookFromState = this.state.books.find(b => b.id === book.id);
if (bookFromState) {
// update existing
bookFromState.shelf = shelf;
this.setState(currentState => ({
books: currentState.books
}));
} else {
// add new one
// the following lines of yours were different
book.shelf = shelf;
this.setState(prevState => ({
books: prevState.books.concat(book)
}));
}
BooksAPI.update(book, shelf);
};
So you merely had books: prevState.books instead of actually concatenating the book to the prev state. And just before that the shelf of book has to be changed to the one you pass.
PS: I might have left some console.log statements. Hope that is not a problem and you will clean the mess.

Categories