I just started learning javascript and react-redux, and I'm using useEffect to call POST method. So, I am wondering how to make it not send request to my backend whenever I open or refresh website
my HTTP Post looks like:
export const sendItemData = (items) => {
return async () => {
const sendRequest = async () => {
const response = await fetch("http://localhost:51044/api/Items", {
method: "POST",
body: JSON.stringify(items.Items),
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "same-origin",
});
if (!response.ok) {
throw new Error("Sending data failed!");
}
};
try {
await sendRequest();
} catch (error) {
console.log(error);
}
};
};
and my App.js looks like:
import React from "react";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import Items from "./components/Items ";
import { sendItemData } from "./store/items-actions";
function App() {
const dispatch = useDispatch();
const sendItems = useSelector((state) => state.items);
useEffect(() => {
dispatch(sendItemData(sendItems));
}, [sendItems, dispatch]);
return <Items />;
}
export default App;
Ok, this is my way, tray it
export function MyComp(props) {
const initial = useRef(true)
useEffect(() => {
if (!initial.current) {
// Yoyr code
} else {
// Mark for next times
initial.current = false;
}
}, [args]);
return (
<YourContent />
);
}
Related
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 component 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>Welcome</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>;
}
Create 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 component 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 incoming 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>Welcome</h1>),
},
{
path: "/dashboard",
element: (
<AuthProvider apiClient={apiClient}> // <-- pass apiClient
<Dashboard />
</AuthProvider>
),
loader: dashboardLoader(apiClient), // <-- pass apiClient
},
]);
I am testing api request call using jest and react testing library , here is my codes.
Live demo live demo
utils/api.js
import axios from "axios";
const instance = axios.create({
withCredentials: true,
baseURL: process.env.REACT_APP_API_URL,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
export default instance;
export async function get(url, params) {
const response = await instance({
method: "GET",
url: url,
params: params,
});
return response;
}
mock/api.js
const mockResponse = {
data: {
attributes: {
first_name: "Jeff",
last_name: "Bezo's",
},
},
};
export default {
get: jest.fn().mockResolvedValue(mockResponse),
};
Home.js
import React, { useState, useEffect } from "react";
function Home() {
const [user, setUser] = useState();
const getUser = useCallback(async () => {
try {
const response = await api.get("/users/self");
console.log("data response..", response.data);
setUser(response.data.attributes.first_name);
} catch (error) {}
}, [dispatch]);
useEffect(() => {
getUser();
}, []);
return <div data-testid="user-info" > Welcome {user}</div>;
}
export default Home;
Home.test.js
import React, { Suspense } from "react";
import { render, cleanup, screen } from "#testing-library/react";
import "#testing-library/jest-dom/extend-expect";
import Home from "../../editor/Home";
import { Provider } from "react-redux";
import { store } from "../../app/store";
import Loader from "../../components/Loader/Loader";
const MockHomeComponent = () => {
return (
<Provider store={store}>
<Suspense fallback={<Loader />}>
<Home />
</Suspense>
</Provider>
);
};
describe("Home component", () => {
it("should return a user name", async () => {
render(<MockHomeComponent />);
const userDivElement = await screen.findByTestId(`user-info`);
expect(userDivElement).toBeInTheDocument();
screen.debug();
});
afterAll(cleanup);
});
Problem:
When I run npm run test test is passed but in the screen.debug() results I dont see the user name returned as expected I just see Welcome. but it should be welcome Jeff.
What am I doing wrong here? any suggestions will be appreciated
I have a custom hook
import { useState } from "react";
import { useDispatch } from "react-redux";
import axios from "axios";
import getDevices from "../actions/devicesAtions";
import { isPositiveInteger, FORM_FIELDS } from "../helper";
export default function useDevice(value) {
const dispatch = useDispatch();
const [device, setDevice] = useState(value);
const [msg, setMsg] = useState("");
const saveDeviceChange = ({ target }) => {
const { name, value } = target;
setDevice({
...device,
[name]: value
});
setMsg("");
};
const saveDeviceSubmit = async (
e,
axiosMethod,
selectedUrl,
newDevice = device
) => {
e.preventDefault();
const { system_name, type, hdd_capacity } = device;
if (!system_name || !type || !hdd_capacity) {
setMsg(
`Please fill out ${!system_name ? FORM_FIELDS.SYS_NAME : ""} ${
!type ? FORM_FIELDS.DEVICE_TYPE : ""
} ${!hdd_capacity ? FORM_FIELDS.HDD_CAPACITY : ""}!`
);
return false;
}
if (!isPositiveInteger(hdd_capacity)) {
setMsg(
"Please enter a positive number or round it to the nearst whole number!"
);
return false;
}
try {
await axios({
method: axiosMethod,
url: selectedUrl,
data: device,
headers: {
"Content-Type": "application/json"
}
});
dispatch(getDevices());
} catch (err) {
console.log(err);
}
setMsg("Changes have been made!");
setDevice(newDevice);
};
return {
device,
setDevice,
msg,
setMsg,
saveDeviceChange,
saveDeviceSubmit
};
}
The EditDeviceWrapper component uses the states and functions from the custom hook. When this component renders, the selectedDevice is assigned as the value for the device that's from the custom hook. When first rendered, the values are displayed correctly on the from. However, after I clicked refresh, the device state from the custom hook disappear while the selectedDevice state from the redux still exits. How to maintain the device state from the custom hook after refreshing the the component?
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useParams } from "react-router-dom";
import ReusedForm from "./ReusedForm";
import useDevice from "./useDevice";
import { getDeviceDetails } from "../actions/devicesAtions";
export default function EditDeviceWrapper() {
const dispatch = useDispatch();
const { selectedDevice } = useSelector((state) => state.allDevices);
const { id } = useParams();
useEffect(() => {
dispatch(getDeviceDetails(id));
}, [id, dispatch]);
const { device, msg, saveDeviceChange, saveDeviceSubmit } = useDevice(
selectedDevice
);
console.log(device, selectedDevice);
return (
<>
<p className="form-msg">{msg}</p>
<ReusedForm
saveDeviceSubmit={(e) =>
saveDeviceSubmit(e, "put", `http://localhost:3000/devices/${id}`)
}
selectDeviceValChange={saveDeviceChange}
heading="Update Device"
system_name={device.system_name}
type={device.type}
hdd_capacity={device.hdd_capacity}
/>
<p>{id}</p>
</>
);
}
May be persist the state after page refresh this link helps you to resolve your problem.
I want to call a rest api with react-redux but my fetch doesn't called at all.
My actions:
restActions.js file:
export const customerbyidGetAction = (data) => ({ type: types.customerbyidGetAction, data });
My reducer:
restReducer.js file:
import { Map, fromJS } from 'immutable';
import * as types from '../constants/restConstants';
const initialState = {};
const initialImmutableState = fromJS(initialState);
export default function reducer(state = initialImmutableState, action) {
switch(action.type) {
case types.customerbyidGetAction:
return {
...state,
data: action.payload
}
break;
default:
// the dispatched action is not in this reducer, return the state unchanged
return state;
}
}
restApi.js file:
import {customerbyidGetAction} from '../../redux/actions/restActions'
const URL = "https://***"
export default function customerbyidGet() {
return dispatch => {
console.log("not called")
dispatch(customerbyidGetAction());
fetch(URL + 'customer/byid/Get',{
method:'POST',
headers:{
'Accept': 'application/json',
'Content-Type': 'application/json',
'token':1234
},
body: JSON.stringify({'customerId': 1})
})
.then(res => {
const r = res.json();
return r;
})
.then(res => {
if(res.error) {
throw(res.error);
}
dispatch(customerbyidGetAction(res));
return res;
})
.catch(error => {
dispatch(customerbyidGetAction(error));
})
}
}
export default apis;
inside my Component:
import React from 'react';
import { Helmet } from 'react-helmet';
import Grid from '#material-ui/core/Grid';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { withStyles } from '#material-ui/core/styles';
import PropTypes from 'prop-types';
import {
Help, InsertDriveFile,
MonetizationOn,
Person,
PersonPin,
RemoveRedEye
} from '#material-ui/icons';
import { Link } from 'react-router-dom';
import CounterWidget from '../../../components/Counter/CounterWidget';
import colorfull from '../../../api/palette/colorfull';
import styles from '../../../components/Widget/widget-jss';
import PapperBlock from '../../../components/PapperBlock/PapperBlock';
import {customerbyidGetAction} from '../../../redux/actions/restActions';
class Panelclass extends React.Component {
componentDidMount(){
const {customerbyidGet} = this.props;
customerbyidGet()
}
render() {
const { classes } = this.props;
return (
<div>
Hi
</div>
);
}
}
Panelclass.propTypes = {
classes: PropTypes.object.isRequired,
customerbyidGet: PropTypes.func.isRequired,
};
// const mapStateToProps = state => ({
// customers: customerbyidGet(state),
// })
const mapDispatchToProps = dispatch => bindActionCreators({
customerbyidGet: customerbyidGetAction
}, dispatch)
const Panel = connect(
//mapStateToProps,
null,
mapDispatchToProps
)(Panelclass);
export default withStyles(styles)(Panel);
Your reducer expects payload property in action:
function reducer(state = initialImmutableState, action) {
switch (action.type) {
case types.customerbyidGetAction:
return {
...state,
data: action.payload, // Here
};
}
}
So, you need to have that property in action creator:
const customerbyidGetAction = (data) => ({
type: types.customerbyidGetAction,
payload: data, // Here
});
Also, you need to fix the correct action import in component:
import { customerbyidGet } from "../path-to/restApi";
const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
customerbyidGet,
},
dispatch
);
PS: Today, you should use the Redux Toolkit library which reduces lots of boilerplate code.
First of all define your rest api function like below
export default function customerbyidGet(dispatch) {
return () => {
console.log("not called")
dispatch(customerbyidGetAction());
fetch(URL + 'customer/byid/Get',{
method:'POST',
headers:{
'Accept': 'application/json',
'Content-Type': 'application/json',
'token':1234
},
body: JSON.stringify({'customerId': 1})
})
.then(res => {
const r = res.json();
return r;
})
.then(res => {
if(res.error) {
throw(res.error);
}
dispatch(customerbyidGetAction(res));
return res;
})
.catch(error => {
dispatch(customerbyidGetAction(error));
})
}
}
Then pass it to your mapDispatchToProps function
const mapDispatchToProps = dispatch => bindActionCreators({
customerbyidGetData: customerbyidGet(dispatch)
}, dispatch)
And finally`invoke it in mapDispatchToProps
componentDidMount(){
const {customerbyidGetData} = this.props;
customerbyidGetData()
}
The other two answers are pointing out good points, but missing something very crucial: That ; there is a typo.
const {customerbyidGet} = this.props;
customerbyidGet()
should be
const {customerbyidGet} = this.props.customerbyidGet()
Tell me please why i can't to get local data from json in axios.
db.json is at the root of the project, but in the getEvents function it throws error 404.
Help me please
operation.js
import FetchClient from 'app/utils/FetchClient';
import IdsAndByIds from 'app/utils/IdsAndByIds';
import { eventsFetch, setEvents } from './actions';
export const getEvents = () => async (dispatch) => {
try {
const { data } = await FetchClient.get('./db.json');
dispatch(setEvents(IdsAndByIds(data)));
dispatch(eventsFetch(false));
} catch (error) {
console.log(error);
}
};
FetchClient.js
import axios from 'axios';
import { URL_API } from 'app/config'; //localhost:3009
const FetchClient = () => {
const defaultOptions = {
baseURL: URL_API,
method: 'get',
headers: {
'Content-Type': 'application/json'
}
};
const instance = axios.create(defaultOptions);
return instance;
};
export default FetchClient();
actions.js
import * as types from './types';
export const eventsFetch = value => ({
type: types.FETCHING_EVENTS,
payload: value
});
export const setEvents = ({ objById, arrayIds }) => ({
type: types.SET_EVENTS,
payload: {
eventById: objById,
eventsOrder: arrayIds
}
});