Access Object Values with Context API in Child Component - javascript

I have the following object being exported into another file:
info.js
export const info = {
companyName: '',
title: 'Some title',
year: '',
};
I'm importing this object into my Context.js like so:
InfoContext.js
import React, { useState, createContext } from 'react';
import {info} from './info'
export const InfoContext = createContext();
export const InfoProvider = ({ children }) => {
const [state, setState] = useState({
...info,
});
return (
<InfoContext.Provider value={[state, setState]}>
{children}
</InfoContext.Provider>
);
};
What I want to do is access the object values from my state inside my App.js. - Here is what I have tried but I am not having any success:
App.js
import React from "react";
import { InfoProvider, InfoContext } from "./InfoContext";
export default function App() {
return (
<InfoProvider>
<InfoContext.Consumer>
{state => (
<>
<h1>{state.title}</h1>
</>
)}
</InfoContext.Consumer>
</InfoProvider>
);
}
I'm clearly missing something obvious here. I've tried a few things but I'm not sure what the issue is. I feel it has something to do with my object being accessed from a separate file.
Additionally, here is a sandbox link with the above code. Any help would be greatly appreciated!
Code Sandbox

You are passing your value to Provider as array, but on Consumer you expecting it to be an Object.
You need to pass an Object instead:
<InfoContext.Provider value={{state, setState}}>
Also you are using Consumer wrong. As a callback it takes whole value that you've passed in Provider, not state:
<InfoContext.Consumer>
{(value) => (
<>
<h1>{value.state.title}</h1>
</>
)}
</InfoContext.Consumer>
or using destructured assignment:
<InfoContext.Consumer>
{({state}) => (
<>
<h1>{state.title}</h1>
</>
)}
</InfoContext.Consumer>
then you can use value.setState({...}) for example. etc. But note that this is a bad practice updating state like that.
Code Sandbox

Related

In React Router V6, can I still get RouteComponentProps (or access to history and location) in class components? [duplicate]

The version of react-router-dom is v6 and I'm having trouble with passing values to another component using Navigate.
I want to pass selected rows to another page called Report. But, I'm not sure I'm using the right syntax for navigate method and I don't know how to get that state in the Report component.
Material-ui Table: I'm trying to use redirectToReport(rowData) in onClick parameter.
function TableRows(props){
return (
<MaterialTable
title="Leads"
columns={[
...
]}
data = {props.leads}
options={{
selection: true,
filtering: true,
sorting: true
}}
actions = {[{
position: "toolbarOnSelect",
tooltip: 'Generate a report based on selected leads.',
icon: 'addchart',
onClick: (event, rowData) => {
console.log("Row Data: " , rowData)
props.redirect(rowData)
}
}]}
/>
)}
LeadTable component
export default function LeadTable(props) {
let navigate = useNavigate();
const [leads, setLeads] = useState([]);
const [loading, setLoading] = useState(true);
async function fetchUrl(url) {
const response = await fetch(url);
const json = await response.json();
setLeads(json[0]);
setLoading(false);
}
useEffect(() => {
fetchUrl("http://localhost:5000/api/leads");
}, []);
function redirectToReport(rowData) {
navigate('/app/report', { state: rowData }); // ??? I'm not sure if this is the right way
}
return(
<div>
<TableRows leads={leads} redirect={redirectToReport}></TableRows>
</div>
)}
Report component
export default function ReportPage(state) {
return (
<div>
{ console.log(state) // This doesn't show anything. How to use the state that were passed from Table component here?}
<div className = "Top3">
<h3>Top 3 Leads</h3>
<ReportTop3 leads={[]} />
</div>
</div>
);}
version 6 react-router-dom
I know the question got answered but I feel this might be helpful example for those who want to use functional components and they are in search of passing data between components using react-router-dom v6.
Let's suppose we have two functional components, first component A, second component B. The component A wants to share data to component B.
usage of hooks: (useLocation,useNavigate)
import {Link, useNavigate} from 'react-router-dom';
function ComponentA(props) {
const navigate = useNavigate();
const toComponentB=()=>{
navigate('/componentB',{state:{id:1,name:'sabaoon'}});
}
return (
<>
<div> <a onClick={()=>{toComponentB()}}>Component B<a/></div>
</>
);
}
export default ComponentA;
Now we will get the data in Component B.
import {useLocation} from 'react-router-dom';
function ComponentB() {
const location = useLocation();
return (
<>
<div>{location.state.name}</div>
</>
)
}
export default ComponentB;
Note: you can use HOC if you are using class components as hooks won't work in class components.
Your navigate('/app/report', { state: rowData }); looks correct to me.
react-router-v6
If you need state, use navigate('success', { state }).
navigate
interface NavigateFunction {
(
to: To,
options?: { replace?: boolean; state?: any }
): void;
(delta: number): void;
}
Your ReportPage needs to be rendered under the same Router that the component doing the push is under.
Route props are no longer passed to rendered components, as they are now passed as JSX literals. To access route state it must be done so via the useLocation hook.
function ReportPage(props) {
const { state } = useLocation();
console.log(state);
return (
<div>
<div className="Top3">
<h3>Top 3 Leads</h3>
<ReportTop3 leads={[]} />
</div>
</div>
);
}
If the component isn't able to use React hooks then you still access the route state via a custom withRouter Higher Order Component. Here's an example simple withRouter HOC to pass the location as a prop.
import { useLocation, /* other hooks */ } from 'react-router-dom';
const withRouter = WrappedComponent => props => {
const location = useLocation();
// other hooks
return (
<WrappedComponent
{...props}
{...{ location, /* other hooks */ }}
/>
);
};
Then access via props as was done in pre-RRDv6.
class ReportPage extends Component {
...
render() {
console.log(this.props.location.state);
return (
<div>
<div className="Top3">
<h3>Top 3 Leads</h3>
<ReportTop3 leads={[]} />
</div>
</div>
);
}
}
2 things (just a suggestion):
Rather than a ternary use &&
{location && <div>{location.state.name}</div>}
Why are you checking location and rendering location.state.name? I would use the check on the data you are fetching or make sure the data returns null or your value.
On Sabaoon Bedar's Answer, you can check if there is any data or not before showing it :
Instead of this <div>{location.state.name}</div>
Do this { location != null ? <div>{location.state.name}</div> : ""}
if you want to send data with usenavigate in functional component you can use like that
navigate(`/take-quiz/${id}`, { state: { quiz } });
and you can get it with uselocation hook like this
const location = useLocation();
location.state.quiz there is your data
But you cannot get this data in props it;s tricky part ;)!!
on SABAOON BEDAR answer,
from component A: navigate('/', {state:"whatever"}
in component B: console.log(location.state) //output = whatever

React Context value not defined inside click handler

I have a functional component in which I am trying to use context, I get the value during import, but when I want to use it inside click handler, says its not defined
component.js
// I get these values over here
const [contextState, setContextState] = useContext(MyContext);
const clickHandler = (e) => {
debugger; // when I am trying to log any of these values I get error of not defined
}
return (<button onClick={clickHandler}>Click me!</button>);
Moreover I have wrapped my app.js components as below
import { MyContextProvider } from './MyContext';
return (
<Router>
<MyContextProvider>
<div className='App'>
<Component />
</div>
</AuthContextProvider>
</Router>
);
Now my context file, myContext.js
import React, { useState, createContext } from 'react';
const MyContext = createContext([{}, () => {}]);
const MyContextProvider = (props) => {
const [contextState, setContextState] = useState({
userIsLoggedin: false,
fName: '',
lName: '',
userName: ''
});
return (
<myContext.Provider value={[contextState, setContextState]}>
{props.children}
</myContext.Provider>
);
};
export { myContext, MyContextProvider };
It looks like you have some typos in your context file. First, use MyContext everywhere in that file (not myContext), and second, pay attention to the variable names you're specifying when you first call useState and then pass that same value to the context (authState vs contextState):
import React, { useState, createContext } from "react";
const MyContext = createContext([{}, () => {}]);
const MyContextProvider = props => {
const [authState, setAuthState] = useState({
userIsLoggedin: false,
fName: "",
lName: "",
userName: ""
});
return (
<MyContext.Provider value={[authState, setAuthState]}>
{props.children}
</MyContext.Provider>
);
};
export { MyContext as default, MyContextProvider };
Here's a working codesandbox with your setup
So, I have debugged over and over again only to find out that it is not reading in debugger but value is getting printed in console.log() and while using it in code directly.
It appears there is some issue with chrome debugger in the current version.
So, anyone facing this issue please try using the variable or log it if on chrome
update : it doesn't print in debugger, but it works with log. Probably it is expected, but it works
I think, You Need to Bind the event listener or use array func,
return (<button onClick={clickHandler.bind(this)}>Click me!</button>);
// Or,
return (<button onClick={ev => clickHandler(ev)}>Click me!</button>);

Display list of trails in React - "Warning: Failed prop type"

React Newbie
I am coding in React. I am taking an object of JSON data from a GET request to an api, and trying to pass it as a prop in a component. Then I am mapping over it to make a list of "trail" objects.
I am getting this error in the console:
"Warning: Failed prop type: Invalid prop trail of type array supplied to TrailItem, expected object."
Here's the code for my app level component:
import React, { Component } from "react";
import "./App.css";
import Navbar from "./components/layout/Navbar";
import Trails from "./components/trails/Trails";
import axios from "axios";
class App extends Component {
state = {
trails: {},
};
async componentDidMount() {
const res = await axios.get(
`https://www.hikingproject.com/data/get-trails?lat=35.0844&lon=-106.6504&maxDistance=10&key=${process.env.REACT_APP_HIKING_PROJECT_KEY}`
);
console.log(res.data);
this.setState({ trails: res.data });
}
render() {
return (
<div className='App'>
<Navbar />
<div>
<Trails trails={this.state.trails} />
</div>
</div>
);
}
}
export default App;
As far as I can tell, there is no problem with the data. A console.log(res.data); returns an object, so I know the api request is working.
Here's the code for my "Trails" component:
import React, { Component } from "react";
import TrailItem from "./TrailItem";
class Trails extends Component {
render() {
return (
<div style={trailStyle}>
{Object.keys(this.props.trails).map((key) => (
<TrailItem key={key} trail={this.props.trails[key]} />
))}
</div>
);
}
}
const trailStyle = {
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gridGap: "1rem",
};
export default Trails;
I feel like maybe I'm not using the correct syntax to step into the object, and then further into the "trails" array, but I'm stumped. Thank you for you help!
EDIT
Here is the "TrailItem" code:
import React from "react";
import PropTypes from "prop-types";
const TrailItem = ({ trail: { name, location, imgSmall } }) => {
return (
<div className='card text-center'>
<img src={imgSmall} alt='trail' style={{ width: "25%" }} />
<h3>{name}</h3>
<p>{location}</p>
</div>
);
};
TrailItem.propTypes = {
trail: PropTypes.array.isRequired,
};
export default TrailItem;
I followed the advice of one of the comments and changed the PropType to array, and that fixed one of the warnings. But I still can't get a list of <TrailItem />.
Inside you App render method put this at the start:
if (!this.state.trails.length) {
return <div>Loading...</div>;
}
You can give trails a default or initial value of an empty array so the map function can be invoked. By using an empty array the component will map over an the empty array and return an empty array to render.
Default Value:
const Trails = ({ trails }) => {
console.log(trails.trails);
return (
<div style={trailStyle}>
{trails.map(trail => <TrailItem key={trail.id} trail={trail} />)}
</div>
);
};
TrailItem.propTypes = {
trail: PropTypes.array.isRequired,
};
TrailItem.defaultValue = {
trail: [],
};
Initial Value:
const Trails = ({ trails = [] }) => {
console.log(trails.trails);
return (
<div style={trailStyle}>
{trails.map(trail => <TrailItem key={trail.id} trail={trail} />)}
</div>
);
};
TrailItem.propTypes = {
trail: PropTypes.array.isRequired,
};
Note: This won't fix passing a prop of the incorrect type, but the prop validation react does will. It sounds like you got that bit sorted out though.
I solved it! Granted, I may not have been clear in my original question, but I figured out why I couldn't get access to the data object from the API.
I needed to step into the object one more time upon receiving the response in my App component:
this.setState({ trails: res.data.trails });
Once I did that, in my Trails component I needed Object.key() to make turn the "trails" prop into an array so I could .map() over it.
And finally, the "tricky" part was that I needed to use each "key" as the index for each "trail" prop I was trying to pass to <TrailItem />:
{Object.keys(trails).map((key) => (
<TrailItem key={key} trail={trails[key]} />
))}

Gatsby Context API doesn't render its value

Trying to render state from Context API, but in console it shows as undefined and doesn't render anything.
here is Context file
import React, { useReducer, createContext } from "react"
export const GlobalStateContext = createContext()
export const GlobalDispatchContext = createContext()
const initialState = {
isLoggedIn: "logged out",
}
function reducer(state, action) {
switch (action.type) {
case "TOGGLE_LOGIN":
{
return {
...state,
isLoggedIn: state.isLoggedIn === false ? true : false,
}
}
break
default:
throw new Error("bad action")
}
}
const GlobalContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<GlobalStateContext.Provider value={state}>
{children}
</GlobalStateContext.Provider>
)
}
export default GlobalContextProvider
and here is where value should be rendered
import React, { useContext } from "react"
import {
GlobalStateContext,
GlobalDispatchContext,
} from "../context/GlobalContextProvider"
const Login = () => {
const state = useContext(GlobalStateContext)
console.log(state)
return (
<>
<GlobalStateContext.Consumer>
{value => <p>{value}</p>}
</GlobalStateContext.Consumer>
</>
)
}
export default Login
I tried before the same thing with class component but it didn't solve the problem. When I console log context it looks like object with undefined values.
Any ideas?
The Context API In General
From the comments, it seems the potential problem is that you're not rendering <Login /> as a child of <GlobalContextProvider />. When you're using a context consumer, either as a hook or as a function, there needs to be a matching provider somewhere in the component tree as its parent.
For example, these would not work:
<div>
<h1>Please log in!</h1>
<Login />
</div>
<React.Fragment>
<GlobalContextProvider />
<Login />
</React.Fragment>
because in both of those, the Login component is either a sibling of the context provider, or the provider is missing entirely.
This, however, would work:
<React.Fragment>
<GlobalContextProvider>
<Login />
</GlobalContextProvider>
</React.Fragment>
because the Login component is a child of the GlobalContextProvider.
Related To Gatsby
This concept is true regardless of what library or framework you're using to make your app. In Gatsby specifically there's a little bit of work you have to do to get this to work at a page level, but it's possible.
Let's say you have a Layout.jsx file defined, and the following page:
const Index = () => (
<Layout>
<h1>{something that uses context}</h1>
</Layout>
)
You have 2 options:
The easier option is to extract that h1 into its own component file. Then you can put the GlobalContextProvider in the Layout and put the context consumer in the new component. This would work because the h1 is being rendered as a child of the layout.
Is to do some shuffling.
You might be inclined to put the Provider in the layout and try to consume it in the page. You might think this would work because the h1 is still being rendered as a child of the Layout, right? That is correct, but the context is not being consumed by the h1. The context is being rendered by the h1 and consumed by Index, which is the parent of <Layout>. Using it at a page level is possible, but what you would have to do is make another component (IndexContent or something similar), consume your context in there, and render that as a child of layout. So as an example (with imports left out for brevity):
const Layout = ({children}) => (
<GlobalContextProvider>
{children}
</GlobalContextProvider>
);
const IndexContent = () => {
const {text} = useContext(GlobalStateContext);
return <h1>{text}</h1>;
}
const Index = () => (
<Layout>
<IndexContent />
</Layout>
);

React.createContext point of defaultValue?

On the React 16 Context doc page, they have examples that look similar to this one:
const defaultValue = 'light'
const SomeContext = React.createContext(defaultValue)
const startingValue = 'light'
const App = () => (
<SomeContext.Provider theme={startingValue}>
Content
</SomeContext.Provider>
)
It seems that the defaultValue is useless because if you instead set the startingValue to anything else or don't set it (which is undefined), it overrides it. That's fine, it should do that.
But then what's the point of the defaultValue?
If I want to have a static context that doesn't change, it would be nice to be able to do something like below, and just have the Provider been passed through the defaultValue
const App = () => (
<SomeContext.Provider>
Content
</SomeContext.Provider>
)
When there's no Provider, the defaultValue argument is used for the function createContext. This is helpful for testing components in isolation without wrapping them, or testing it with different values from the Provider.
Code sample:
import { createContext, useContext } from "react";
const Context = createContext( "Default Value" );
function Child() {
const context = useContext(Context);
return <h2>Child1: {context}</h2>;
}
function Child2() {
const context = useContext(Context);
return <h2>Child2: {context}</h2>;
}
function App() {
return (
<>
<Context.Provider value={ "Initial Value" }>
<Child /> {/* Child inside Provider will get "Initial Value" */}
</Context.Provider>
<Child2 /> {/* Child outside Provider will get "Default Value" */}
</>
);
}
Codesandbox Demo
Just sharing my typical setup when using TypeScript, to complete answer from #tiomno above, because I think many googlers that ends up here are actually looking for this:
interface GridItemContextType {
/** Unique id of the item */
i: string;
}
const GridItemContext = React.createContext<GridItemContextType | undefined>(
undefined
);
export const useGridItemContext = () => {
const gridItemContext = useContext(GridItemContext);
if (!gridItemContext)
throw new Error(
'No GridItemContext.Provider found when calling useGridItemContext.'
);
return gridItemContext;
};
The hook provides a safer typing in this scenario. The undefined defaultValue protects you from forgetting to setup the provider.
My two cents:
After reading this instructive article by Kent C. Dodds as usual :), I learnt that the defaultValue is useful when you destructure the value returned by useContext:
Define the context in one corner of the codebase without defaultValue:
const CountStateContext = React.createContext() // <-- define the context in one corner of the codebase without defaultValue
and use it like so in a component:
const { count } = React.useContext(CountStateContext)
JS will obviously say TypeError: Cannot read property 'count' of undefined
But you can simply not do that and avoid the defaultValue altogether.
About tests, my teacher Kent has a good point when he says:
The React docs suggest that providing a default value "can be helpful
in testing components in isolation without wrapping them." While it's
true that it allows you to do this, I disagree that it's better than
wrapping your components with the necessary context. Remember that
every time you do something in your test that you don't do in your
application, you reduce the amount of confidence that test can give
you.
Extra for TypeScript; if you don't want to use a defaultValue, it's easy to please the lint by doing the following:
const MyFancyContext = React.createContext<MyFancyType | undefined>(undefined)
You only need to be sure to add the extra validations later on to be sure you have covered the cases when MyFancyContext === undefined
MyFancyContext ?? 'default'
MyFancyContext?.notThatFancyProperty
etc
You can set the default values using useReducer hook, then the 2nd argument will be the default value:
import React, { createContext, useReducer } from "react";
import { yourReducer } from "./yourReducer";
export const WidgetContext = createContext();
const ContextProvider = (props) => {
const { children , defaultValues } = props;
const [state, dispatch] = useReducer(yourReducer, defaultValues);
return (
<WidgetContext.Provider value={{ state, dispatch }}>
{children}
</WidgetContext.Provider>
);
};
export default ContextProvider;
// implementation
<ContextProvider
defaultValues={{
disabled: false,
icon: undefined,
text: "Hello",
badge: "100k",
styletype: "primary",
dir: "ltr",
}}
>
</ContextProvider>

Categories