Dynamically generating query params with react-router v6 and useNavigate - javascript

I'm trying to figure out how to use the useNavigate hook to navigate/redirect a user and update the query params, if there are any.
I've created a custom useNavigateParams hook that I've adapted from several different SO answers and it looks like this:
import { generatePath, ParamKeyValuePair, useNavigate } from 'react-router-dom';
import { getSearchParams, isObjectEmpty } from 'src/utils';
type TUseNavigateParams = {
uri: string;
params?: Record<string, unknown>;
};
export default function useNavigateParams() {
const navigate = useNavigate();
return ({ uri, params = {} }: TUseNavigateParams) => {
let path = uri;
if (!isObjectEmpty(params)) {
path += generatePath('?:queryString', {
uri,
queryString: getSearchParams(Object.entries(params) as ParamKeyValuePair[]),
});
}
console.log('decoded', decodeURIComponent(path));
navigate(path);
};
}
const getSearchParams = (params: ParamKeyValuePair[]) => {
let searchParams = '';
params.forEach((param, index) => {
const localDestructured = Object.entries(param[1]);
console.log('localDestructured', localDestructured);
searchParams += createSearchParams({
[param[0]]: JSON.stringify({ [localDestructured[0][0]]: localDestructured[0][1] }),
});
});
return createSearchParams(searchParams).toString();
};
The idea here is that I would essentially always use useNavigateParams throughout my project, instead of useNavigate.
My issue is the following. Something simple like the following would work fine:
const uri = 'myPath'
const filter = 'active'
const params = { filter: { status: filter } }
navigate({
uri,
params,
})
This would print,
myPath?filter={"status":"active"}
but doing something like,
const uri = 'myPath'
const filter = 'active'
const params = { filter: { status: filter, hello: 'world' } }
navigate({
uri,
params,
})
would not print,
myPath?filter={"status":"active", "hello":"world"}
I understand that I could add another forEach or figure out some hacky approach, but it just seems so bulky as it is and it is effectively in no way reusable/dynamic.
I'd like to be able to do pass in some sort of params, such as,
const uri = 'myPath'
const filter = 'active'
const params = { filter: { status: filter, hello: 'world' }, foo: 'bar', baz: { node: 'leaf' } }
navigate({
uri,
params,
})
and expect the output to be:
myPath?filter={"status":"active", "hello":"world"}&foo=bar&baz={"node":"leaf"}
Is there a cleaner and more dependable approach to achieve such a result?

I was already using axios, so I ended up using getUri:
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
type TUseNavigateParams = {
uri: string;
params?: Record<string, unknown>;
};
export default function useNavigateParams() {
const navigate = useNavigate();
return ({ uri, params = {} }: TUseNavigateParams) => {
const path = axios.getUri({ url: uri, params });
navigate(path);
};
}

Related

I have some issue about using useRouter | Next js

This is my first time using useRouter and i really dont know how it works.
I want to take props.indexUrl and added dynamiacally into the url below, i have done several ways but always getting error.
import { useRouter } from "next/router";
const Component = () => {
const router = useRouter();
const {
query: { indexUrl },
} = router;
const props = {
indexUrl,
};
return props.indexUrl;
};
export const urls = [
{
url:
"click.myyellowlocal.com/k.php?ai=19202&url=http%3a%2f%2f" +
Component,
},
{
url: "cse.google.vu/url?q=https%3A%2F%2",
},
{
url: "sazhs.co.kr/member/login.html?noMemberOrder=&returnUrl=http%3a%2f%2",
},
];
I have try like this before
import { useRouter } from "next/router";
const router = useRouter();
const {
query: { indexUrl },
} = router;
const props = {
indexUrl,
};
export const urls = [
{
url:
"click.myyellowlocal.com/k.php?ai=19202&url=http%3a%2f%2f" +
props.indexUrl,
},
{
url: "cse.google.vu/url?q=https%3A%2F%2",
},
{
url: "sazhs.co.kr/member/login.html?noMemberOrder=&returnUrl=http%3a%2f%2",
},
];
and give an error Cannot read properties of null (reading 'useContext')
so i make like this
const Component = () => {
const router = useRouter();
const {
query: { indexUrl },
} = router;
const props = {
indexUrl,
};
};
but i dont know how to get props.indexUrl to url like in the top code.
more detail, the router indexUrl is from other file
function sendProps() {
Router.push({
// pathname: "/processUrl",
pathname: "/urlList",
query: { indexUrl },
});
}
how to get the value of props.indexUrl and add it into the list of url dynamiacally ??

NextJs how to solve Objects are not valid as a React child (found: object with keys {children}) issue

I'm building a nextjs-application and I crossed an issue with getStaticPaths. Inside the pages-folder, I have a file called [slug].tsx which contains this code:
import { Image } from "react-datocms";
import { request } from "../lib/datocms";
import { GetStaticProps, GetStaticPaths } from "next";
export default function Page({ pageData }) {
return (
<div>
<h1>{pageData.title}</h1>
</div>
);
}
const PATHS_QUERY = `
query MyQuery {
allPages {
slug
}
}
`;
export const getStaticPaths: GetStaticPaths = async (context) => {
const slugQuery = await request({
query: PATHS_QUERY,
preview: context.preview,
});
let paths = [];
slugQuery.allPages.map((path) => paths.push(`/${path.slug}`));
return {
paths,
fallback: false,
};
};
const PAGE_QUERY = `
query MyQuery($slug: String) {
page(filter: {slug: {eq: $slug}}) {
title
slug
id
}
}
`;
export const getStaticProps: GetStaticProps = async ({ params }) => {
const page = {
query: PAGE_QUERY,
variables: { slug: params.slug },
};
return {
props: {
pageData: page,
}
};
};
This gives me the error: Objects are not valid as a React child (found: object with keys {children}). If you meant to render a collection of children, use an array instead.
I have no clue what this means, so can anyone help me out?
****** UPDATE ******
I suspect my Navbar could have something to do with this. In my components folfer, I have a nav folder with a Navbar.tsx-file which looks like this:
const Navbar = ({ topNav }) => {
const menu_items = topNav.menuItems[0];
return (
<nav>
{menu_items.topNavigationItems.map((navitem, idx) => (
<div key={navitem.text}>
<NavItem {...navitem} />
</div>
))}
</nav>
)
}
export default Navbar;
the NavItem looks like this:
const NavItem = ({ text, path, active }) => {
return (
<Link href={path.slug}>
<a>
{text}
</a>
</Link>
);
};
export default NavItem;
The way you are building your paths array inside getStaticPaths is not quite right according to the new standards. You have to "push" an object with a key of params, which then contains an object with your slug.
Rewriting your getStaticPaths function would result in the following.
export const getStaticPaths: GetStaticPaths = async (context) => {
const slugQuery = await request({
query: PATHS_QUERY,
preview: context.preview,
});
const paths = slugQuery.allPages.map(path => {params: {slug: path.slug} });
return {
paths,
fallback: false,
};
};
You can read more about the getStaticPaths function in the official documentation.
EDIT: To be more specific on the error you're getting, you are trying to render an object as a JSX element, thus generating an error. Try and find the source of that error and fix it this way.

How to rewrite or change the slug using getStaticProps for only one slug using Next js

I am working on my Next JS project. I have all my slugs which I map through and create all pages using the catch-all route from Next. However, I have a page with slug /home I would like to rewrite this path to the root path like so "/" How can I change the slug inside the getStaticProps? Currently I have to go to /home to see my homepage but I need this to be the absolute root path.
Code:
import { getClient, usePreviewSubscription } from "../lib/sanity";
import { groq } from "next-sanity";
import { getQueryFromSlug } from "../lib/helpers";
import dynamic from "next/dynamic";
const PageSingle = dynamic(() => import("../components/layouts/PageSingle"));
const NewsSingle = dynamic(() => import("../components/layouts/NewsSingle"));
interface Props {
data: any;
preview: boolean;
}
export default function Page({ data, preview }: Props) {
const { data: pageData } = usePreviewSubscription(data?.query, {
params: data?.queryParams ?? {},
initialData: data?.pageData,
enabled: preview,
});
const { docType } = data;
return (
<>
{docType === "home" && <PageSingle page={pageData} />}
{docType === "page" && <PageSingle page={pageData} />}
{docType === "article" && <NewsSingle post={pageData} />}
{docType === "case" && <NewsSingle post={pageData} />}
</>
);
}
export async function getStaticPaths() {
const pageQueries = await getClient().fetch(
groq`*[_type in ["homePage", "page", "article", "case"] && defined(slug.current)][].slug.current`
);
// Split the slug strings to arrays (as required by Next.js)
const paths = pageQueries.map((slug: string) => ({
params: { slug: slug.split("/").filter((p) => p) },
}));
return { paths, fallback: false };
}
export async function getStaticProps({ params }: any) {
// Every website has a bunch of global content that every page needs, too!
// const globalSettings = await client.fetch(globalSettingsQuery);
// A helper function to work out what query we should run based on this slug
const { query, queryParams, docType } = getQueryFromSlug(params.slug);
// Get the initial data for this page, using the correct query
const pageData = await getClient().fetch(query, queryParams);
return {
props: {
data: { query, queryParams, docType, pageData },
},
};
}

How to use a custom React hook to make a POST or DELETE request with Axios

I am trying to make a generic useAxios hook in React. I would like to be able to import this hook into other components to make Get, Post, and Delete requests. I have created the hook and it works fine for making Get requests, but I am stuck on how to make it work for Post/Delete requests.
The issue is that I would be making the Post/Delete request when a user clicks a Save or Delete button, but I cannot call a React hook from an event handler function or from useEffect.
Below is the generic hook I created:
import { useState, useEffect } from "react";
import axios from "axios";
export interface AxiosConfig<D> {
method?: 'get' | 'post' | 'delete' | 'put';
url: string;
data?: D;
params?: URLSearchParams;
}
export const useAxios = <T, D = undefined >(config: AxiosConfig<D>) => {
const [responseData, setResponseData] = useState<T>();
const [isLoading, setIsloading] = useState(true);
const [isError, setIsError] = useState(false);
useEffect(() => {
const controller = new AbortController();
const axiosRequest = async () => {
try {
const response = await axios({ ...config, signal: controller.signal })
setResponseData(response.data)
setIsloading(false);
} catch (error) {
setIsError(true);
setIsloading(false);
}
}
axiosRequest();
return () => {
controller.abort();
}
}, [config.url, config.method, config.data, config.params])
return {responseData, isLoading, isError}
}
And this is an example of a component where I would like to make a Delete request
import { useParams } from 'react-router';
import { useAxios } from '../../api/hooks/useAxios';
export interface IItem {
title: string;
info: string;
}
export default function Item() {
const { id } = useParams<{id?: string}>();
const {responseData: item, isLoading, isError} = useAxios<IItem>({
method: 'get',
url: `http://localhost:3000/items/${id}`
})
const handleDelete = () => {
//not sure what to do here. Need to make DELETE request
}
return (
<div>
{isLoading && <p className='loading'>Loading...</p>}
{isError && <p className='error'>Could Not Load Item</p>}
{item && (
<>
<h2>{item.title}</h2>
<p>{item.info}</p>
<button onClick={handleDelete}>Delete</button>
</>
)}
</div>
)
}
I could just make the axios request directly in the Item component and not use my useAxios hook, but then I would end up repeating code throughout the application.
Assuming your DELETE route is the same as the GET route, you'd just store the method type in a local state variable and change it:
const { id } = useParams<{id?: string}>();
const [method, setMethod] = useState('get');
const {responseData: item, isLoading, isError} = useAxios<IItem>({
method,
url: `http://localhost:3000/items/${id}`
});
const handleDelete = () => setMethod('delete');
However, I think you will realize that this only solves part of the problem, which is that you have tightly coupled your component's return JSX with the response type of the GET request (IItem).

Cannot access a nested array within an object in react-redux

Hello, I am new to redux and I am struggling with a problem. I am trying to access and map over the comments within my post array. However, I am not sure how to do this. So far, I've tried changing the actions and reducers in order to solve this issue. I think the problem is within the react and redux. I can't tell if my mapStateToProps is working correctly. Also, the state is being fetched from my express server and it seems to be working properly as you can see in the picture.
My getPost action:
export const getPost = (group_id, post_id) => async dispatch => {
try {
const res = await axios.get(`/api/groups/${group_id}/${post_id}`);
dispatch({
type: GET_POST,
payload: res.data
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.statusText, status: error.response.status }
});
}
};
The initial state:
const initialState = {
groups: [],
group: [],
loading: true,
error: {}
};
The reducer:
case GET_POST:
return {
...state,
post: payload,
loading: false
};
Where I'm trying to map over the comments:
import React, { Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getPost } from '../../../redux/actions/group';
const Post = ({ getPost, post, match }) => {
useEffect(() => {
getPost(match.params.group_id, match.params.post_id);
}, [getPost, match.params.group_id, match.params.post_id]);
// I want to map over the comments here
return (
{post.comments.map(comment => ({ comment }))}
);
};
Post.propTypes = {
getPost: PropTypes.func.isRequired,
group: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
post: state.post
});
export default connect(mapStateToProps, { getPost })(Post);
You can access nested object with some tricks using redux, we have use this way in our prod env for some time.
First the reducer (you can make this reducer even more complex)
const LocalStorageReducer = createReducer<Store['localStorage']>(
new LocalStorage(),
{
saveLocalStorageItem(state: LocalStorage, action: any) {
return {...state, [action.payload.item]: action.payload.value}; // <= here
},
}
);
For Actions
export const actions = {
saveLocalStorageItem: (payload: InputAction) => ({type: 'saveLocalStorageItem', payload}),
};
For the type InputAction
export class InputAction {
item: string;
value: string | Array<string> | null | boolean;
constructor() {
this.item = '';
this.value = null;
}
}
For the handler in component
this.props.saveLocalStorage({ item: 'loading', value: false });
In this way you can go one way done to the nested redux store.
For complex (4-5 levels) and multiple (> 2 times) data structure, there are other ways, but in most situations, it's good enough.

Categories