Mega Animenia
Mega Animenia

Reputation: 15

Code doesnt run after having 2 hooks with null properties

This part of the code is not working after I updated react scripts from 2.0 to 5.0.

  const { user, dispatch } = useContext(AuthContext);
  const { data } = useFetch(`/contracts/${user.contractType}`);

  if (!user) {
    return <Navigate to="/" />;
  }

What I want to happen is if a user that isn't logged in tries to access this page via URL, they get redirected to the main website page. The error that I get from the console:

TypeError: Cannot read properties of null (reading 'contractType') at Userinfo (userinfo.js:25:1)

Here is the full code of the page

import React, { useContext } from "react";
import { useState } from "react";
import useFetch from "../../hooks/useFetch";
import Footer from "../../components/OutFooter";
import Navbar from "../../components/OutNavbar";
import Sidebar from "../../components/OutSidebar";
import {
  ContractContainer,
  HeadingContainer,
  TypeH1,
  ActiveUntil,
  MonthlyWrapper,
  MonthlyContainer,
  MonthNumber,
  Price,
  Navbarback,
} from "./userinfoElements";
import { AuthContext } from "../../context/AuthContext";
import { Navigate } from "react-router-dom";
import moment from "moment";
import axios from "axios";

const Userinfo = () => {
  const { user, dispatch } = useContext(AuthContext);
  const { data } = useFetch(`/contracts/${user.contractType}`);

  // for nav bars
  const [isOpen, setIsOpen] = useState(false);

  // set state to true if false
  const toggle = () => {
    setIsOpen(!isOpen);
  };

  if (!user) {
    return <Navigate to="/" />;
  }

  let dateFormat = moment(user.activeUntil).format("DD/MMMM/yyyy");

  const update1Month = async () => {
    try {
      let newDate = moment(user.activeUntil).add(30, "days");
      dateFormat = newDate.format("DD/MMMM/yyyy");
      await axios.put(`/activedate/${user.namekey}`, {
        activeUntil: newDate,
      });
      dispatch({ type: "UPDATE_USER_DATE", payload: newDate });
    } catch (err) {
      console.log(err);
    }
  };

  const update3Month = async () => {
    try {
      let newDate = moment(user.activeUntil).add(90, "days");
      dateFormat = newDate.format("DD/MMMM/yyyy");
      await axios.put(`/activedate/${user.namekey}`, {
        activeUntil: newDate,
      });
      dispatch({ type: "UPDATE_USER_DATE", payload: newDate });
    } catch (err) {
      console.log(err);
    }
  };
  const update6Month = async () => {
    try {
      let newDate = moment(user.activeUntil).add(180, "days");
      dateFormat = newDate.format("DD/MMMM/yyyy");
      await axios.put(`/activedate/${user.namekey}`, {
        activeUntil: newDate,
      });
      dispatch({ type: "UPDATE_USER_DATE", payload: newDate });
    } catch (err) {
      console.log(err);
    }
  };
  const update12Month = async () => {
    try {
      let newDate = moment(user.activeUntil).add(365, "days");
      dateFormat = newDate.format("DD/MMMM/yyyy");
      await axios.put(`/activedate/${user.namekey}`, {
        activeUntil: newDate,
      });
      dispatch({ type: "UPDATE_USER_DATE", payload: newDate });
    } catch (err) {
      console.log(err);
    }
  };

  return (
    <>
      <Sidebar isOpen={isOpen} toggle={toggle} />
      {/* navbar for smaller screens*/}
      <Navbar toggle={toggle} />
      <Navbarback /> {/* filling for transparent bacground navbar*/}
      <>
        <ContractContainer>
          <TypeH1>
            Hello {user.fName} {user.lName}!
          </TypeH1>
          <HeadingContainer>
            <TypeH1>{data.contractType}</TypeH1>
            <ActiveUntil>Subscription active until {dateFormat}</ActiveUntil>
          </HeadingContainer>
          <MonthlyWrapper>
            <MonthlyContainer>
              <MonthNumber>1 Month</MonthNumber>
              <Price onClick={update1Month}>{data.month1Price}$</Price>
            </MonthlyContainer>
            <MonthlyContainer>
              <MonthNumber>3 Month</MonthNumber>
              <Price onClick={update3Month}>{data.month3Price}$</Price>
            </MonthlyContainer>
            <MonthlyContainer>
              <MonthNumber>6Month</MonthNumber>
              <Price onClick={update6Month}>{data.month6Price}$</Price>
            </MonthlyContainer>
            <MonthlyContainer>
              <MonthNumber>12Month</MonthNumber>
              <Price onClick={update12Month}>{data.month12Price}$</Price>
            </MonthlyContainer>
          </MonthlyWrapper>
        </ContractContainer>
      </>
      <Footer />
    </>
  );
};

export default Userinfo;

Addition: The code works as I want it to when I add the if statement between the 2 hooks like this

const { user, dispatch } = useContext(AuthContext);
 if (!user) {
      return <Navigate to="/" />;
    }
  const { data } = useFetch(`/contracts/${user.contractType}`);
 
   

But then I get an error that react hook is being used conditionally. In the case before this where the if statement is used after the hooks, the code after const { data } = useFetch(/contracts/${user.contractType}); doesn't run (tried with console.log), is there a way I can make the statement at least get recognized after the useFetch hook is used? Or if not possible, is there a way to make it so that when a console error happens I can redirect to the main page or ignore the console error?

EDIT: Adding files relevant to this.

AuthContext

import React from "react";
import { createContext, useEffect, useReducer } from "react";

const INITIAL_STATE = {
  user: JSON.parse(localStorage.getItem("user")) || null,
  loading: false,
  error: null,
};

export const AuthContext = createContext(INITIAL_STATE);

const AuthReducer = (state, action) => {
  switch (action.type) {
    case "LOGIN_START":
      return {
        user: null,
        loading: true,
        error: null,
      };
    case "LOGIN_SUCCESS":
      return {
        user: action.payload,
        loading: false,
        error: null,
      };
    case "LOGIN_FAILURE":
      return {
        user: null,
        loading: false,
        error: action.payload,
      };
    case "LOGOUT":
      return {
        user: null,
        loading: false,
        error: null,
      };
    case "UPDATE_USER_DATE":
      const updatedUser = { ...state.user };
      updatedUser.activeUntil = action.payload;
      return {
        ...state,
        user: updatedUser,
      };

    default:
      return state;
  }
};

export const AuthContextProvider = ({ children }) => {
  const [state, dispatch] = useReducer(AuthReducer, INITIAL_STATE);

  useEffect(() => {
    localStorage.setItem("user", JSON.stringify(state.user));
  }, [state.user]);

  return (
    <AuthContext.Provider
      value={{
        user: state.user,
        loading: state.loading,
        error: state.error,
        dispatch,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

useFetch

import { useEffect, useState } from "react";
import axios from "axios";

const useFetch = (url) => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const res = await axios.get(url);
        setData(res.data);
      } catch (err) {
        setError(err);
      }
      setLoading(false);
    };
    fetchData();
  }, [url]);

  const reFetch = async () => {
    setLoading(true);
    try {
      const res = await axios.get(url);
      setData(res.data);
    } catch (err) {
      setError(err);
    }
    setLoading(false);
  };

  return { data, loading, error, reFetch };
};

export default useFetch;

API users mongoose schema

import mongoose from "mongoose";
const UserSchema = new mongoose.Schema({
  fName: { type: String },
  lName: { type: String },
  namekey: {
    type: String,
    required: true,
    unique: true,
  },
  password: {
    type: String,
    required: true,
  },
  contractType: {
    type: String,
  },
  activeUntil: {
    type: Date,
  },
});

export default mongoose.model("User", UserSchema);

API contract mongoose schema

import mongoose from "mongoose";
const ContractsSchema = new mongoose.Schema({
  contractType: {
    type: String,
  },
  speed: {
    type: Number,
  },
  month1Price: {
    type: Number,
  },
  month3Price: {
    type: Number,
  },
  month6Price: {
    type: Number,
  },
  month12Price: {
    type: Number,
  },
  promote: {
    type: Boolean,
  },
});

export default mongoose.model("Contracts", ContractsSchema);

API authentication of users

import User from "../models/User.js";
import bcrypt from "bcryptjs";
import { createError } from "../utils/error.js";
import jwt from "jsonwebtoken";

// export const register = async (req, res, next) => {
//   try {
//     const salt = bcrypt.genSaltSync(13);
//     const hash = bcrypt.hashSync(req.body.password, salt);

//     const newUser = new User({
//       ...req.body,
//       password: hash,
//     });

//     await newUser.save();
//     res.status(200).send("User has been created.");
//   } catch (err) {
//     next(err);
//   }
// };
export const login = async (req, res, next) => {
  try {
    const user = await User.findOne({ namekey: req.body.namekey });
    if (!user) return next(createError(404, "User not found!"));
    if (req.body.password === undefined) {
      return next(createError(500, "Wrong password or namekey!"));
    }
    const isPasswordCorrect = await bcrypt.compare(
      req.body.password,
      user.password
    );
    if (!isPasswordCorrect)
      return next(createError(400, "Wrong password or namekey!"));

    const token = jwt.sign({ id: user._id }, process.env.JWT);

    const { password, ...otherDetails } = user._doc;
    res
      .cookie("access_token", token, {
        httpOnly: true,
      })
      .status(200)
      .json({ details: { ...otherDetails } });
  } catch (err) {
    next(err);
  }
};

App.js routes

import React from "react";
import "./App.css";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/index";
import Signin from "./pages/signin/Signin";
import Userinfo from "./pages/userInfo/userinfo";
import PageNotFound from "./pages/PageNotFound";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route exact path="/" element={<Home />} />
        <Route exact path="/signin" element={<Signin />} />
        <Route exact path="/myinfo" element={<Userinfo />} />
        <Route path="*" element={<PageNotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

src index.js

import React from "react";
import ReactDOM from "react-dom/client";
import { AuthContextProvider } from "./context/AuthContext";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <AuthContextProvider>
      <App />
    </AuthContextProvider>
  </React.StrictMode>
);

Upvotes: 1

Views: 78

Answers (1)

Drew Reese
Drew Reese

Reputation: 202751

The initial user state is potentially null:

const INITIAL_STATE = {
  user: JSON.parse(localStorage.getItem("user")) || null,
  ...
};

The AuthReducer functional also sets the user state to null in several cases:

const AuthReducer = (state, action) => {
  switch (action.type) {
    case "LOGIN_START":
      return {
        user: null, // <-- here
        loading: true,
        error: null,
      };
    case "LOGIN_SUCCESS":
      return {
        user: action.payload,
        loading: false,
        error: null,
      };
    case "LOGIN_FAILURE":
      return {
        user: null, // <-- here
        loading: false,
        error: action.payload,
      };
    case "LOGOUT":
      return {
        user: null, // <-- here
        loading: false,
        error: null,
      };
    case "UPDATE_USER_DATE":
      const updatedUser = { ...state.user };
      updatedUser.activeUntil = action.payload;
      return {
        ...state,
        user: updatedUser,
      };

    default:
      return state;
  }
};

With the existing code you should account for the potentially null user state object by using a null-check/guard-clause when passing the value to the useFetch hook.

Example:

const { user, dispatch } = useContext(AuthContext);
const { data, reFetch } = useFetch(`/contracts/${user?.contractType}`);

...

const useFetch = (url) => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);

  const const fetchData = React.useCallback(async () => {
    setLoading(true);
    try {
      const res = await axios.get(url);
      setData(res.data);
    } catch (err) {
      setError(err);
    }
    setLoading(false);
  }, [url]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { data, loading, error, reFetch: fetchData };
};

Upvotes: 1

Related Questions