For the last 2 hours, I've read unnumerous posts from StackOverflow, medium, and other independent blogs, and I haven't been able to crack or decipher how to properly mock a simple custom useAuth() hook.
I'm getting:
[TypeError: Cannot destructure property 'user' of '(0 , _auth.useAuth)(...)' as it is undefined.]
Here's my code:
The <Dashboard/> component which includes the useAuth() hook. (Code omitted due to brevity)
import { useAuth } from '../../../auth';
export const Dashboard: React.FC<RouteComponentProps> = (props) => {
const { user } = useAuth();
The dashboard.test.tesx file.
import { render, waitFor } from "../../../__tests_setup__/app-test-utils";
// Sets Up useAuth
import { mockPatientDetails } from "../../../setupTestPatient";
import { mockBaseAuth } from "../../../setupTestShared";
import { Dashboard } from "./index";
import { useAuth } from "../../../auth";
jest.mock("../../../auth");
describe("Tests the Patient's dashboard", () => {
beforeAll(() => {
(useAuth as any).mockReturnValue({
...mockBaseAuth,
user: {
...mockBaseAuth.user,
profile: mockPatientDetails.profile,
profile_id: mockPatientDetails.profile_id,
username: mockPatientDetails.username,
},
});
});
it("Will be able to assign a consultation now", async () => {
const renderer = await render(<Dashboard />);
waitFor(async () => {
await expect(renderer.getByText(`Hola ${mockPatientDetails.username}`));
});
expect(true).toBe(true);
});
});
Other variations tried:
import { render, waitFor } from "../../../__tests_setup__/app-test-utils";
// Sets Up useAuth
import { mockPatientDetails } from "../../../setupTestPatient";
import { mockBaseAuth } from "../../../setupTestShared";
import { Dashboard } from "./index";
import { useAuth } from "../../../auth";
// Variation 1
import * as auth from '../../../auth'
jest.spyOn(auth, "useAuth").mockImplementation(() => ({
...mockBaseAuth,
user: {
...mockBaseAuth.user,
profile: mockPatientDetails.profile,
profile_id: mockPatientDetails.profile_id,
username: mockPatientDetails.username,
},
}));
// Variation 2
jest.mock("../../../auth/index", () => ({
__esModule: true,
useAuth: jest.fn(() => ({
...mockBaseAuth,
user: {
...mockBaseAuth.user,
profile: mockPatientDetails.profile,
profile_id: mockPatientDetails.profile_id,
username: mockPatientDetails.username,
},
})),
}));
// Variation 3
There have been many other variations which I haven't included as I've completely forgotten about them.
Here's my folder structure, just in case.
P.S: Here are the variables shown above:
src/setupTestShared.ts
import { GENDER, InteractionMedias, UserProfile } from "./#dts";
import { useAuth } from "./auth";
const success = Promise.resolve({
type: "success" as const,
result: true,
});
export const mockBaseAuth: ReturnType<typeof useAuth> = {
login(u: string, p: string) {
return success;
},
authenticated: true,
logout() {},
register(p: UserProfile, pass: string) {
return success;
},
userExists(u: string) {
return Promise.resolve("USER_REGISTERED" as const);
},
user: {
date_of_birth: "1990-12-21",
email: "test#gmail.com",
emails: ["test#gmail.com"],
first_name: "Maria",
gender: GENDER.FEMALE,
id: 1,
identification_id: "402-2066666-1",
interaction_media_preferred: InteractionMedias.VIDEO,
last_name: "Anabelle",
loggedInDate: new Date(),
phones: ["809-544-5111"],
profile: "ANONYMOUS",
profile_id: 1,
username: "anonymous",
},
};
export const mockPatientDetails = {
username: "PAC123456",
profile: "patient" as const,
profile_id: 2,
};
What could it be?
It's working now!
This answer helped me:
https://stackoverflow.com/a/60282832/1057052
// https://stackoverflow.com/a/60282832/1057052
jest.mock("../../../auth", () => ({
// this isn't needed - __esModule: true,
useAuth: () => ({
...mockBaseAuth,
user: {
...mockBaseAuth.user,
profile: mockPatientDetails.profile,
profile_id: mockPatientDetails.profile_id,
username: mockPatientDetails.username,
},
}),
}));
The trick was not assigning the jest.fn() to the useAuth().
Related
I have a React Context, so I can save some data and reuse it where I want in my React-Project. Here is the code I'm working on:
import React, { useState, createContext } from "react"
import apiRequest from "../axios"
import { getCookie } from "../Utils"
import jwt_decode from "jwt-decode"
export const UserContext = React.createContext()
export const UserProvider = (props) => {
const [value, setValue] = useState({
loading: false,
isLoggedIn: false,
userId: "",
username: ""
})
const updateData = (toUpdate) => {
setValue({...value, ...toUpdate})
}
const fetchUser = async (userId, key) => {
await apiRequest("get", "/user/" + userId, null, () => {}, (data) => {
updateData({
loading: false,
isLoggedIn: true,
userId: userId,
username: data.user.username
})
}, (errMessage) => {
updateData({
loading: false
})
}, key)
}
// load user data if access token is set
const accessToken = getCookie("access")
if (accessToken && !value.loggedIn && !value.loading) {
updateData({ loading: true })
const { sub: userId } = jwt_decode(accessToken)
fetchUser(userId, accessToken) // if I comment this out, then no infinite loop
}
const methods = {
fetchUser,
updateData
}
return (
<UserContext.Provider value={[value, methods]}>
{props.children}
</UserContext.Provider>
)
}
I have commented the line, where it creates this loop. Can anyone tell me why it is behaving like that?
You need to do the fetch request in the useEffect so that it is fired only when the component is mounted or when the cookie value changes.
Try this;
import React, { useState, createContext, useEffect } from "react"
import apiRequest from "../axios"
import { getCookie } from "../Utils"
import jwt_decode from "jwt-decode"
export const UserContext = React.createContext()
export const UserProvider = (props) => {
const [value, setValue] = useState({
loading: false,
isLoggedIn: false,
userId: "",
username: ""
})
const updateData = (toUpdate) => {
setValue({...value, ...toUpdate})
}
const fetchUser = async (userId, key) => {
updateValue({ loading: true });
await apiRequest("get", "/user/" + userId, null, () => {}, (data) => {
updateData({
loading: false,
isLoggedIn: true,
userId: userId,
username: data.user.username
})
}, (errMessage) => {
updateData({loading: false})
}, key)
}
// load user data if access token is set
const accessToken = getCookie("access")
useEffect(() => {
if (accessToken && !value.loggedIn && !value.loading) {
const { sub: userId } = jwt_decode(accessToken)
fetchUser(userId, accessToken);
}
}, [accessToken, value.loggedId, value.loading]);
const methods = {
fetchUser,
updateData
}
return (
<UserContext.Provider value={[value, methods]}>
{props.children}
</UserContext.Provider>
)
}
I'm using the hook below for auth with AWS Cognito. When a user signs in, it doesn't run and therefore React thinks the user is still not authenticated. If I refresh the page, everything works as it should. What am I doing wrong?
import { useState, useEffect, useMemo } from "react";
import { Auth } from "aws-amplify";
interface User {
email: string;
id: string;
isAuthenticated: boolean;
loading: boolean;
}
const useAuth = () => {
const [user, setUser] = useState<User>({
email: "",
id: "",
isAuthenticated: false,
loading: true,
});
const auth = useMemo(() => {
Auth.configure({
userPoolId: process.env.REACT_APP_COGNITO_USER_POOL_ID,
userPoolWebClientId: process.env.REACT_APP_COGNITO_APP_CLIENT_ID,
});
return Auth;
}, []);
useEffect(() => {
const checkUser = async () => {
try {
const user = await auth.currentAuthenticatedUser();
setUser({
email: user.attributes.email,
id: user.attributes.sub,
isAuthenticated: true,
loading: false,
});
return null;
} catch (e) {
setUser({
email: "",
id: "",
isAuthenticated: false,
loading: false,
});
return null;
}
};
checkUser();
}, []);
return user;
};
export default useAuth;
I have a problem where I can load the user data from my node server, but when I try to get the data into State in the frontend of React, I get a 404 when I call the data.
error: http://localhost:3000/users 404 (Not Found)
I have tried several approaches but it seems that my issue lies in not being able to pre-load the data from the database into State....can anyone please tell me what I'm missing?
Routes/API
// #route GET api/users
// #desc Get Users
// #access Public
router.get('/', async (req, res) => {
try {
const users = await User.find();
res.json(users);
} catch (error) {
console.error(error.message);
res.status(500).send('Server Error');
}
});
module.exports = router;
Then on the frontend, I have an action
import axios from 'axios';
import {
GET_USERS,
GET_USERS_ERROR
} from './types';
// Get users
export const getUsers = () => async (dispatch) => {
try {
const res = await axios.get('/users');
dispatch({
type: GET_USERS,
payload: res.data,
});
} catch (error) {
dispatch({
type: GET_USERS_ERROR,
payload: {
msg: error.response.status.statusText,
status: error.response.status,
},
});
}
};
My reducer file:
import {
GET_USERS,
GET_USERS_ERROR
} from '../actions/types';
const initialState = {
user: null,
users: [],
error: {},
};
export default function(state = initialState, action) {
const {
type,
payload
} = action;
switch (type) {
case GET_USERS:
return {
...state,
users: payload,
};
case GET_USERS_ERROR:
return {
...state,
error: payload,
};
default:
return state;
}
}
finally, the place where I'm trying to get the data
import React, {
useState,
useEffect
} from 'react';
import {
connect
} from 'react-redux';
import {
getUsers
} from '../../actions/users';
import PropTypes from 'prop-types';
//Bootstrap Table
import BootstrapTable from 'react-bootstrap-table-next';
import paginationFactory from 'react-bootstrap-table2-paginator';
import * as ReactBootStrap from 'react-bootstrap';
const UserTable = ({
getUsers,
users
}) => {
useEffect(() => {
getUsers();
// eslint-disable-next-line
}, []);
const [loading, setLoading] = useState(false);
const columns = [{
dataField: '_id',
text: 'ID'
},
{
dataField: 'user_id',
text: "User's ID"
},
{
dataField: 'firstname',
text: 'Title of Todo'
},
{
dataField: 'lastname',
text: 'Is this done?'
},
];
return ( <
div > Hello < /div>
// <BootstrapTable
// keyField='id'
// data={users}
// columns={columns}
// pagination={paginationFactory()}
// />
);
};
const mapStateToProps = (state) => ({
users: state.users,
});
export default connect(mapStateToProps, {
getUsers
})(UserTable);
Based on this bit in you question // #route GET api/users, indicates you are likely missing /api in the FE call.
Try
const res = await axios.get('/api/users');
hope someone can point me the right direction with this. Basically I've created a react app which makes use of hooks, specifically useContext, useEffect and useReducer. My problem is that I can't seem to get tests to detect click or dispatch events of the related component.
Stripped down version of my app can be found at : https://github.com/atlantisstorm/hooks-testing
Tests relate to layout.test.js script.
I've tried various approaches, different ways of mocking dispatch, useContext, etc but no joy with it. Most recent version.
layout.test.js
import React from 'react';
import { render, fireEvent } from "#testing-library/react";
import Layout from './layout';
import App from './app';
import { Provider, initialState } from './context';
const dispatch = jest.fn();
const spy = jest
.spyOn(React, 'useContext')
.mockImplementation(() => ({
state: initialState,
dispatch: dispatch
}));
describe('Layout component', () => {
it('starts with a count of 0', () => {
const { getByTestId } = render(
<App>
<Provider>
<Layout />
</Provider>
</App>
);
expect(dispatch).toHaveBeenCalledTimes(1);
const refreshButton = getByTestId('fetch-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(3);
});
});
layout.jsx
import React, { useContext, useEffect } from 'react';
import { Context } from "./context";
const Layout = () => {
const { state, dispatch } = useContext(Context);
const { displayThings, things } = state;
const onClickDisplay = (event) => {
// eslint-disable-next-line
event.preventDefault;
dispatch({ type: "DISPLAY_THINGS" });
};
useEffect(() => {
dispatch({ type: "FETCH_THINGS" });
}, [displayThings]);
const btnText = displayThings ? "hide things" : "display things";
return (
<div>
<button data-testid="fetch-button" onClick={onClickDisplay}>{btnText}</button>
{ displayThings ?
<p>We got some things!</p>
:
<p>No things to show!</p>
}
{ displayThings && things.map((thing) =>
<p>{ thing }</p>
)}
</div>
)
}
export default Layout;
app.jsx
import React from 'react';
import Provider from "./context";
import Layout from './layout';
const App = () => {
return (
<Provider>
<Layout />
</Provider>
)
}
export default App;
context.jsx
import React, { createContext, useReducer } from "react";
import { reducer } from "./reducer";
export const Context = createContext();
export const initialState = {
displayThings: false,
things: []
};
export const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
);
};
export default Provider;
reducer.jsx
export const reducer = (state, action) => {
switch (action.type) {
case "DISPLAY_THINGS": {
const displayThings = state.displayThings ? false : true;
return { ...state, displayThings };
}
case "FETCH_THINGS": {
const things = state.displayThings ? [
"thing one",
"thing two"
] : [];
return { ...state, things };
}
default: {
return state;
}
}
};
I'm sure the answer will be easy when I see it, but just trying to figure out I can detect the click event plus detect the 'dispatch' events? (I've already got separate test in the main app to properly test dispatch response/actions)
Thank you in advance.
EDIT
Ok, I think I've got a reasonable, though not perfect, solution. First I just added optional testDispatch and testState props to the context.jsx module.
new context.jsx
import React, { createContext, useReducer } from "react";
import { reducer } from "./reducer";
export const Context = createContext();
export const initialState = {
displayThings: false,
things: []
};
export const Provider = ({ children, testDispatch, testState }) => {
const [iState, iDispatch] = useReducer(reducer, initialState);
const dispatch = testDispatch ? testDispatch : iDispatch;
const state = testState ? testState : iState;
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
);
};
export default Provider;
Then in layout.test.jsx I just simply pass in mocked jest dispatch function plus state as necessary. Also removed the outer App wrapping as that seemed to prevent the props from being passed through.
new layout.test.jsx
import React from 'react';
import { render, fireEvent } from "#testing-library/react";
import Layout from './layout';
import { Provider } from './context';
describe('Layout component', () => {
it('starts with a count of 0', () => {
const dispatch = jest.fn();
const state = {
displayThings: false,
things: []
};
const { getByTestId } = render(
<Provider testDispatch={dispatch} testState={state}>
<Layout />
</Provider>
);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: "FETCH_THINGS" });
const refreshButton = getByTestId('fetch-things-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(2);
// Important: The order of the calls should be this, but dispatch is reporting them
// the opposite way around in the this test, i.e. FETCH_THINGS, then DISPLAY_THINGS...
//expect(dispatch).toHaveBeenNthCalledWith(1, { type: "DISPLAY_THINGS" });
//expect(dispatch).toHaveBeenNthCalledWith(2, { type: "FETCH_THINGS" });
// ... so as dispatch behaves correctly outside of testing for the moment I'm just settling for
// knowing that dispatch was at least called twice with the correct parameters.
expect(dispatch).toHaveBeenCalledWith({ type: "DISPLAY_THINGS" });
expect(dispatch).toHaveBeenCalledWith({ type: "FETCH_THINGS" });
});
});
One little caveat though, as noted above, when the 'fetch-things-button' was fired, it reported the dispatch in the wrong order. :/ So I just settled for knowing the correct calls where triggered, but if anyone knows why the call order isn't as expected I would be pleased to know.
https://github.com/atlantisstorm/hooks-testing update to reflect the above if anyone is interested.
A few months back I was also trying to write unit tests for reducer + context for an app. So, here's my solution to test useReducer and useContext.
FeaturesProvider.js
import React, { createContext, useContext, useReducer } from 'react';
import { featuresInitialState, featuresReducer } from '../reducers/featuresReducer';
export const FeatureContext = createContext();
const FeaturesProvider = ({ children }) => {
const [state, dispatch] = useReducer(featuresReducer, featuresInitialState);
return <FeatureContext.Provider value={{ state, dispatch }}>{children}</FeatureContext.Provider>;
};
export const useFeature = () => useContext(FeatureContext);
export default FeaturesProvider;
FeaturesProvider.test.js
import React from 'react';
import { render } from '#testing-library/react';
import { renderHook } from '#testing-library/react-hooks';
import FeaturesProvider, { useFeature, FeatureContext } from './FeaturesProvider';
const state = { features: [] };
const dispatch = jest.fn();
const wrapper = ({ children }) => (
<FeatureContext.Provider value={{ state, dispatch }}>
{children}
</FeatureContext.Provider>
);
const mockUseContext = jest.fn().mockImplementation(() => ({ state, dispatch }));
React.useContext = mockUseContext;
describe('useFeature test', () => {
test('should return present feature toggles with its state and dispatch function', () => {
render(<FeaturesProvider />);
const { result } = renderHook(() => useFeature(), { wrapper });
expect(result.current.state.features.length).toBe(0);
expect(result.current).toEqual({ state, dispatch });
});
});
featuresReducer.js
import ApplicationConfig from '../config/app-config';
import actions from './actions';
export const featuresInitialState = {
features: [],
environments: ApplicationConfig.ENVIRONMENTS,
toastMessage: null
};
const { INITIALIZE_DATA, TOGGLE_FEATURE, ENABLE_OR_DISABLE_TOAST } = actions;
export const featuresReducer = (state, { type, payload }) => {
switch (type) {
case INITIALIZE_DATA:
return {
...state,
[payload.name]: payload.data
};
case TOGGLE_FEATURE:
return {
...state,
features: state.features.map((feature) => (feature.featureToggleName === payload.featureToggleName
? {
...feature,
environmentState:
{ ...feature.environmentState, [payload.environment]: !feature.environmentState[payload.environment] }
}
: feature))
};
case ENABLE_OR_DISABLE_TOAST:
return { ...state, toastMessage: payload.message };
default:
return { ...state };
}
};
featuresReducer.test.js
import { featuresReducer } from './featuresReducer';
import actions from './actions';
const { INITIALIZE_DATA, TOGGLE_FEATURE, ENABLE_OR_DISABLE_TOAST } = actions;
describe('Reducer function test', () => {
test('should initialize data when INITIALIZE_DATA action is dispatched', () => {
const featuresState = {
features: []
};
const action = {
type: INITIALIZE_DATA,
payload: {
name: 'features',
data: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
}
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
});
});
test('should toggle the feature for the given feature and environemt when TOGGLE_FEATURE action is disptched', () => {
const featuresState = {
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}, {
featureId: '23458', featureName: 'WO photo download', featureToggleName: '23458_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
};
const action = {
type: TOGGLE_FEATURE,
payload: { featureToggleName: '23456_WOPhotoDownload', environment: 'sit' }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}, {
featureId: '23458', featureName: 'WO photo download', featureToggleName: '23458_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
});
});
test('should enable the toast message when ENABLE_OR_DISABLE_TOAST action is dispatched with the message as part of payload', () => {
const featuresState = {
toastMessage: null
};
const action = {
type: ENABLE_OR_DISABLE_TOAST,
payload: { message: 'Something went wrong!' }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({ toastMessage: 'Something went wrong!' });
});
test('should disable the toast message when ENABLE_OR_DISABLE_TOAST action is dispatched with message as null as part of payload', () => {
const featuresState = {
toastMessage: null
};
const action = {
type: ENABLE_OR_DISABLE_TOAST,
payload: { message: null }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({ toastMessage: null });
});
test('should return the current state when the action with no specific type is dispatched', () => {
const featuresState = {
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}]
};
const action = {};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}]
});
});
});
enter code hereI'm providing Redux Global State to my whole react app through a Provider wrapper in my app.js file.
I've no problem accessing any other piece of state other than "Current Profile".
Here is the component:
import React, { Fragment, useEffect } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { loadTargetProfiles, loadCurrentProfile } from "../../actions/profile";
const Friends = ({
loadCurrentProfile,
loadTargetProfiles,
profile: { currentProfile, targetProfiles, targetProfilesAreLoading },
}) => {
useEffect(() => {
loadTargetProfiles();
loadCurrentProfile();
}, []);
console.log(currentProfile);
return (
...
};
Friends.propTypes = {
loadTargetProfiles: PropTypes.func.isRequired,
loadCurrentProfile: PropTypes.func.isRequired,
profile: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
profile: state.profile,
});
export default connect(mapStateToProps, {
loadCurrentProfile,
loadTargetProfiles,
})(Friends);
here is the loadCurrentProfile action responsible for providing the currentProfile piece of state.
export const loadCurrentProfile = () => async (dispatch) => {
try {
const res = await api.get("/profile/current");
dispatch({
type: LOAD_CURRENT_PROFILE,
payload: res.data,
});
} catch (err) {
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status },
});
}
};
here is the relevant part of the Reducer
const initialState = {
currentProfile: null,
targetProfile: null,
targetProfiles: [],
currentProfileIsLoading: true,
targetProfileIsLoading: true,
targetProfilesAreLoading: true,
error: {},
};
//
// Export Reducer
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
case LOAD_CURRENT_PROFILE:
return {
...state,
currentProfile: payload,
currentProfileIsLoading: false,
};
here is the API that's getting hit:
router.get("/current", auth, async (req, res) => {
try {
const profile = await Profile.findOne({
user: req.user.id,
}).populate("user", ["_id", "username", "registerdate"]);
if (!profile) {
return res
.status(400)
.json({ msg: "There is no profile for this user." });
}
res.json(profile);
} catch (err) {
console.error(err.message);
res.status(500).send("Server Error...");
}
});
here is the console
here is the currentProfile piece of state expanded:
when i try to reach into the currentProfile piece of state, for example
const Friends = ({
loadCurrentProfile,
loadTargetProfiles,
profile: { currentProfile: {
avatar
}, targetProfiles, targetProfilesAreLoading },
}) => {
useEffect(() => {
loadTargetProfiles();
loadCurrentProfile();
}, []);
console.log(avatar);
It give me the error:
TypeError: Cannot read property 'avatar' of null
here is Redux Dev Tools screenshot:
Fix:
I (temporarily) got rid of the error by accessing the inner state after declaring the function.
const Friends= ({
profile: { currentProfileIsLoading },
currentProfile,
}) => {
currentProfile && console.log(currentProfile.avatar);
and it works for now but it certainly isn't the most elegant solution. Is there a way to add this guard in the function declaration in order to set the state in one place?
Issue: Both targetProfiles and targetProfilesAreLoading are truthy values in your state, but currentProfile is null until the GET resolves. You can't access the avatar property of a null object.
You can provide some default argument value for profile, this only works really though if profile is undefined, null counts as a defined value.
const Friends = ({
loadCurrentProfile,
loadTargetProfiles,
profile: {
currentProfile = {},
targetProfiles,
targetProfilesAreLoading
},
}) => {
useEffect(() => {
loadTargetProfiles();
loadCurrentProfile();
}, []);
console.log(currentProfile);
return (
...
};
You can also use a guard on the possibly undefined/null object
currentProfile && currentProfile.avatar
Another alternative is to use a state selector library like reselect that allows you to pull/augment/derive/etc... state values that get passed as props. This also allows you to set default/fallback values for state. It pairs with redux nicely.