admin管理员组文章数量:1332881
I have a JWT-based API. It rotates the tokens on every response. I have a custom provider that manages this.
I'm trying to figure out how I would use React Router v6.4 data router with this setup. Specifically, I'd like to use the loader
/ action
functions for getting the data, but those don't support useContext
and I'm not sure how to pass that in.
I'd like dashboardLoader
to call the API with the current set of tokens as headers that AuthContext
is managing for me.
The goal is to have the loader
function fetch some data to display on the dashboard and to use the get()
call from the AuthProvider
.
My current alternative is to just do it inside the Dashboard
ponent but would like to see how to do this with a loader.
The relevant files:
// App.js
import "./App.css";
import { createBrowserRouter } from "react-router-dom";
import "./App.css";
import Dashboard, { loader as dashboardLoader } from "./dashboard";
import AuthProvider from "./AuthProvider";
import axios from "axios";
function newApiClient() {
return axios.create({
baseURL: "http://localhost:3000",
headers: {
"Content-Type": "application/json",
},
});
}
const api = newApiClient();
export const router = createBrowserRouter([
{
path: "/",
element: (<h1>Wele</h1>),
},
{
path: "/dashboard",
element: (
<AuthProvider apiClient={api}>
<Dashboard />
</AuthProvider>
),
loader: dashboardLoader,
},
]);
// AuthProvider
import { createContext, useState } from "react";
const AuthContext = createContext({
login: (email, password) => {},
isLoggedIn: () => {},
get: async () => {},
post: async () => {},
});
export function AuthProvider(props) {
const [authData, setAuthData] = useState({
client: props.apiClient,
accessToken: "",
});
async function login(email, password, callback) {
try {
const reqData = { email: email, password: password };
await post("/auth/sign_in", reqData);
callback();
} catch (e) {
console.error(e);
throw e;
}
}
function isLoggedIn() {
return authData.accessToken === "";
}
async function updateTokens(headers) {
setAuthData((prev) => {
return {
...prev,
accessToken: headers["access-token"],
};
});
}
async function get(path) {
try {
const response = await authData.client.get(path, {
headers: { "access-token": authData.accessToken },
});
await updateTokens(response.headers);
return response;
} catch (error) {
console.error(error);
throw error;
}
}
async function post(path, data) {
try {
const response = await authData.client.post(path, data, {
headers: { "access-token": authData.accessToken },
});
await updateTokens(response.headers);
return response.data;
} catch (error) {
// TODO
console.error(error);
throw error;
}
}
const context = {
login: login,
isLoggedIn: isLoggedIn,
get: get,
post: post,
};
return (
<AuthContext.Provider value={context}>
{props.children}
</AuthContext.Provider>
);
}
// Dashboard
import { useContext } from "react";
import { Navigate } from "react-router-dom";
import AuthContext from "./AuthProvider";
export function loader() {
// TODO use auth context to call the API
// For example:
// const response = await auth.get("/my-data");
// return response.data;
}
export default function Dashboard() {
const auth = useContext(AuthContext);
if (!auth.isLoggedIn()) {
return <Navigate to="/" replace />;
}
return <h1>Dashboard Stuff</h1>;
}
I have a JWT-based API. It rotates the tokens on every response. I have a custom provider that manages this.
I'm trying to figure out how I would use React Router v6.4 data router with this setup. Specifically, I'd like to use the loader
/ action
functions for getting the data, but those don't support useContext
and I'm not sure how to pass that in.
I'd like dashboardLoader
to call the API with the current set of tokens as headers that AuthContext
is managing for me.
The goal is to have the loader
function fetch some data to display on the dashboard and to use the get()
call from the AuthProvider
.
My current alternative is to just do it inside the Dashboard
ponent but would like to see how to do this with a loader.
The relevant files:
// App.js
import "./App.css";
import { createBrowserRouter } from "react-router-dom";
import "./App.css";
import Dashboard, { loader as dashboardLoader } from "./dashboard";
import AuthProvider from "./AuthProvider";
import axios from "axios";
function newApiClient() {
return axios.create({
baseURL: "http://localhost:3000",
headers: {
"Content-Type": "application/json",
},
});
}
const api = newApiClient();
export const router = createBrowserRouter([
{
path: "/",
element: (<h1>Wele</h1>),
},
{
path: "/dashboard",
element: (
<AuthProvider apiClient={api}>
<Dashboard />
</AuthProvider>
),
loader: dashboardLoader,
},
]);
// AuthProvider
import { createContext, useState } from "react";
const AuthContext = createContext({
login: (email, password) => {},
isLoggedIn: () => {},
get: async () => {},
post: async () => {},
});
export function AuthProvider(props) {
const [authData, setAuthData] = useState({
client: props.apiClient,
accessToken: "",
});
async function login(email, password, callback) {
try {
const reqData = { email: email, password: password };
await post("/auth/sign_in", reqData);
callback();
} catch (e) {
console.error(e);
throw e;
}
}
function isLoggedIn() {
return authData.accessToken === "";
}
async function updateTokens(headers) {
setAuthData((prev) => {
return {
...prev,
accessToken: headers["access-token"],
};
});
}
async function get(path) {
try {
const response = await authData.client.get(path, {
headers: { "access-token": authData.accessToken },
});
await updateTokens(response.headers);
return response;
} catch (error) {
console.error(error);
throw error;
}
}
async function post(path, data) {
try {
const response = await authData.client.post(path, data, {
headers: { "access-token": authData.accessToken },
});
await updateTokens(response.headers);
return response.data;
} catch (error) {
// TODO
console.error(error);
throw error;
}
}
const context = {
login: login,
isLoggedIn: isLoggedIn,
get: get,
post: post,
};
return (
<AuthContext.Provider value={context}>
{props.children}
</AuthContext.Provider>
);
}
// Dashboard
import { useContext } from "react";
import { Navigate } from "react-router-dom";
import AuthContext from "./AuthProvider";
export function loader() {
// TODO use auth context to call the API
// For example:
// const response = await auth.get("/my-data");
// return response.data;
}
export default function Dashboard() {
const auth = useContext(AuthContext);
if (!auth.isLoggedIn()) {
return <Navigate to="/" replace />;
}
return <h1>Dashboard Stuff</h1>;
}
Share
Improve this question
edited Dec 11, 2022 at 4:56
Misha M
asked Dec 11, 2022 at 1:39
Misha MMisha M
11.3k17 gold badges55 silver badges67 bronze badges
4
-
Is
dashboardLoader
referencing some "global" axios "instance" by any chance? TheAuthProvider
could be rendered higher in the ReactTree and set these headers on the axios instance the app is using. Does this make sense? If not, can you edit your post to include a more plete minimal reproducible example for all the relevant code you are using between creating the Data Router and the loader function and ponent? – Drew Reese Commented Dec 11, 2022 at 3:54 -
Right now, I have the
axios
instance on theAuthProvider
. I tried adding a request and a response interceptor for theaxios
client, but couldn't figure out how to pass in the updated tokens. Let me extract the code into something more cohesive – Misha M Commented Dec 11, 2022 at 4:07 - Updated with the example code – Misha M Commented Dec 11, 2022 at 4:56
- This a such a mon usage and I was facing with the same problem. I'm wondering why the authors don't cover this in their documentations or examples. – Mohsen Taleb Commented Mar 3, 2023 at 13:43
1 Answer
Reset to default 4Create the axios instance as you are, but you'll tweak the AuthProvider
to add request and response interceptors to handle the token and header. You'll pass a reference to the apiClient
to the dashboardLoader
loader function as well.
AuthProvider
Store the access token in a React ref and directly consume/reference the passed apiClient
instead of storing it in local ponent state (a React anti-pattern). Add a useEffect
hook to add the request and response interceptors to maintain the accessTokenRef
value.
export function AuthProvider({ apiClient }) {
const accessTokenRef = useRef();
useEffect(() => {
const requestInterceptor = apiClient.interceptors.request.use(
(config) => {
// Attach current access token ref value to outgoing request headers
config.headers["access-token"] = accessTokenRef.current;
return config;
},
);
const responseInterceptor = apiClient.interceptors.response.use(
(response) => {
// Cache new token from ining response headers
accessTokenRef.current = response.headers["access-token"];
return response;
},
);
// Return cleanup function to remove interceptors if apiClient updates
return () => {
apiClient.interceptors.request.eject(requestInterceptor);
apiClient.interceptors.response.eject(responseInterceptor);
};
}, [apiClient]);
async function login(email, password, callback) {
try {
const reqData = { email, password };
await apiClient.post("/auth/sign_in", reqData);
callback();
} catch (e) {
console.error(e);
throw e;
}
}
function isLoggedIn() {
return accessTokenRef.current === "";
}
const context = {
login,
isLoggedIn,
get: apiClient.get,
post: apiClient.post,
};
return (
<AuthContext.Provider value={context}>
{props.children}
</AuthContext.Provider>
);
}
Dashboard
Note here that loader
is a curried function, e.g. a function that consumes a single argument and returns another function. This is to consume and close over in callback scope the instance of the apiClient
.
export const loader = (apiClient) => ({ params, request }) {
// Use passed apiClient to call the API
// For example:
// const response = await apiClient.get("/my-data");
// return response.data;
}
App.js
import { createBrowserRouter } from "react-router-dom";
import axios from "axios";
import "./App.css";
import Dashboard, { loader as dashboardLoader } from "./dashboard";
import AuthProvider from "./AuthProvider";
function newApiClient() {
return axios.create({
baseURL: "http://localhost:3000",
headers: {
"Content-Type": "application/json",
},
});
}
const apiClient = newApiClient();
export const router = createBrowserRouter([
{
path: "/",
element: (<h1>Wele</h1>),
},
{
path: "/dashboard",
element: (
<AuthProvider apiClient={apiClient}> // <-- pass apiClient
<Dashboard />
</AuthProvider>
),
loader: dashboardLoader(apiClient), // <-- pass apiClient
},
]);
本文标签: javascriptReact data routerpass context to loaderStack Overflow
版权声明:本文标题:javascript - React data router - pass context to loader - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1742308542a2450406.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论