I have a class component that's empowered with the HOC withRouter, and connect() to have dispatch available in props
export default withRouter(connect()(MyComponent));
This component is coded as a class component, now, I changed the component to a functional component:
Changed the function header from class "MyComponent expands..." to "const MyComponent = props => {..."
Changed the way the state is created, by using "const [state, setState] = useState(..."
Instead of coding componentDidMount to perform initial operations, I use
useEffect(() => {
getData()
}, []);
Where getData is:
const getData = props => {
const getDocument = async () => {
const {
dispatch,
location: { search }, // Here I get an error, search and
match: { params }, // params are undefined
} = props;
...
}
Changed every function definition, from "handleSuccessAction = message => {..." to "const handleSuccessIndex = response => {..."
Changed all reference to functions from "this.myFunction();" to "myFunction()"
And leaved export default withRouter(connect()(MyComponent)) as is
Begin EDIT:
here is the whole code, once turned into a functional component
------------- all imports ------------------
const DocumentPreview = props => {
const [state, setState] = useState({
document: {
documentType: '',
name: '',
file: {
fileUrl: '',
filename: ''
},
},
file: {
fileUrl: '',
filename: ''
},
});
useEffect(() => {
getDocument();
}, []);
const getDocument = async () => {
const {
dispatch,
location: { search },
match: { params },
} = props;
const options = parseUrlParams(search);
setState({ onRequest: true });
showDocumentPreviewRequest(params.id, {
employeeId: options.employee,
dispatch,
successCallback: handleSuccessIndex
});
};
const handleSuccessIndex = response => {
const { data } = response;
const document = camelCaseRecursive(data);
const file = document.fileInfo;
setState({
document,
file,
onRequest: false
});
};
const { onRequest, document, file, modalBody, modalShow, modalTitle, defaultModalShow } = state;
const { pendingToApprove, workflowRequest } = document;
return (
<>
------- Do my stuff -------------
</>
);
}
export default withRouter(connect()(DocumentPreview))
End EDIT
Why do I get this error?, am I doing it wrong?
I'm changing it to a functional component since I have to load the component on a modal, and as I won't be using router and render props, I think I can get this props by using hooks
Rafael
Related
class Dashboard extends Component {
constructor(props) {
super(props)
this.state = {
assetList: [],
assetList1: [];
}
}
componentDidMount = async () => {
const web3 = window.web3
const LandData=Land.networks[networkId]
if (LandData) {
const landList = new web3.eth.Contract(Land.abi, LandData.address)
this.setState({ landList })
}
}
...
}
In this code the state for landlist is not defines in constructor but setState is used. If I have to convert the code to a function component, what will be the equivalent code?
In React class components, there existed a single state object and you could update it with any properties you needed. State in React function components functions a little differently.
React function components use the useState hook to explicitly declare a state variable and updater function.
You can use a single state, and in this case the functionality would be pretty similar, keeping in mind though that unlike the this.setState of class components, the useState
Example:
const Dashboard = () => {
const [state, setState] = React.useState({
assetList: [],
assetList1: []
});
useEffect(() => {
const web3 = window.web3;
const LandData = Land.networks[networkId];
if (LandData) {
const landList = new web3.eth.Contract(Land.abi, LandData.address);
setState(prevState => ({
...prevState,
landList,
}));
}
}, []);
return (
...
);
};
With the useState hook, however, you aren't limited to a single state object, you can declare as many state variables necessary for your code to function properly. In fact it is recommended to split your state out into the discrete chunks of related state.
Example:
const Dashboard = () => {
const [assetLists, setAssetLists] = React.useState({
assetList: [],
assetList1: []
});
const [landList, setLandList] = React.useState([]);
useEffect(() => {
const web3 = window.web3;
const LandData = Land.networks[networkId];
if (LandData) {
const landList = new web3.eth.Contract(Land.abi, LandData.address);
setLandList(landList);
}
}, []);
return (
...
);
};
const Dashboard = () => {
const [assetList, setAssetList] = useState([])
const [assetList1, setAssetList1] = useState([])
useEffect(() => {
const web3 = window.web3
const LandData = Land.networks[networkId]
if (LandData) {
const landList = new web3.eth.Contract(Land.abi, LandData.address)
setAssetList(landList)
}
}, [])
I am trying make an "easy" weather app exercise, just get data from api and render it. I am using "google api map" to get the location from a post code to a latitude and longitud parameters so I can use those numbers and pass it to "open weather map" api to get the weather for that location.
It is working but with bugs...
First I used redux for "location" and "weather". Redux was working but useSelector() wasnt displaying the data properly.
Then I decide to make it easy, on "search" component I am calling an api an getting the location I need, I am storing it with redux and it is working, on "weatherFullDispaly" component I am calling an api for the "weather" details and just pass it as props for the children to render the data but they are not getting it.
The thing is, while the app is running, when I put a post code I get an error because the children are not receiving the data but, if I comment out the children on the parent component and then comment in again, all the data print perfect.
Any help please???
const WeatherFullDisplay = () => {
const [weatherDetails, setWeatherDetails] = useState();
const currentLocation = useSelector(getLocationData);
useEffect(() => {
getWeatherDetails();
}, []);
const getWeatherDetails = async () => {
const API_KEY = process.env.REACT_APP_OPEN_WEATHER_MAP_API_KEY;
const { lat, lng } = await currentLocation.results[0].geometry.location;
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/onecall?lat=${lat}&lon=${lng}&exclude=minutely&units=metric&appid=${API_KEY}`
);
setWeatherDetails(response.data);
};
return (
<div className="weather-full-display-details">
<WeatherNow weatherDetails={weatherDetails} />
<HourlyWeather weatherDetails={weatherDetails} />
<FiveDaysWeather weatherDetails={weatherDetails} />
</div>
);
};
const FiveDaysWeather = ({ weatherDetails }) => {
const displayDailyWeather = () => {
const daysToShow = [
weatherDetails.daily[1],
weatherDetails.daily[2],
weatherDetails.daily[3],
weatherDetails.daily[4],
weatherDetails.daily[5],
];
return daysToShow.map((day, i) => {
return (
<WeatherSingleCard
key={i}
typeOfCard="daily"
weekDay={moment(day.dt * 1000).format("dddd")}
icon={day.weather[0].icon}
weather={day.weather[0].main}
temp={day.temp.day}
/>
);
});
};
return (
<div className="day-single-cards">{displayDailyWeather()}</div>
);
};
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
locationDetails: "",
};
const locationSlice = createSlice({
name: "location",
initialState,
reducers: {
setLocation: (state, action) => {
state.locationDetails = action.payload;
},
cleanLocation: (state) => {
state.locationDetails = ""
}
},
});
export const { setLocation, cleanLocation } = locationSlice.actions;
export const getLocationData = (state) => state.location.locationDetails;
export default locationSlice.reducer;
const SearchBar = () => {
const [postCode, setPostCode] = useState();
const [locationDetails, setLocationDetails] = useState();
const navigate = useNavigate();
const dispatch = useDispatch();
useEffect(() => {
getLocationDetails();
}, [postCode]);
const getLocationDetails = async () => {
const response = await axios.get(
"https://maps.googleapis.com/maps/api/geocode/json",
{
params: {
components: `country:ES|postal_code:${postCode}`,
region: "ES",
key: process.env.REACT_APP_GOOGLE_API_KEY,
},
}
);
setLocationDetails(response.data);
};
const handleSubmit = (e) => {
e.preventDefault();
dispatch(setLocation(locationDetails));
navigate("/detail-weather");
};
const handleChange = (e) => {
setPostCode(e.target.value);
};
I have created one wrapper component where I put my react context.
Inside that wrapper component I have used useEffect() hook where I fetch values from api and try to update context default values.
In my child component I tried to fetch context values but only default value of that context is fetched. So it seems that useEffect hook didnt updated my context object.
Here is wrapper component:
export const CorporateWrapper = ({ apiBaseUrl, children }) => {
const [corporateContextDefaults, setCorporateContextDefaults] = useState({});
useEffect(() => {
(async () => {
try {
const json = await fetchCorporateUserDetails(apiBaseUrl, getClientSideJwtTokenCookie());
if (json.success !== true) {
console.log(json.message);
return {
notFound: true,
};
}
console.log(json.data);
setCorporateContextDefaults({corporateId: json.data.corporate_id, corporateRole: json.data.corporate_role, corporateAdmin: json.data.corporate_role == 'Admin', corporateSuperAdmin: json.data.corporate_super_admin});
} catch (e) {
console.log(e.message);
}
})();
}, []);
return (
<CorporateProvider value={corporateContextDefaults}>
{children}
</CorporateProvider>
);
};
Here is CorporateProvider component:
import React, { useState, useContext } from "react";
const CorporateContext = React.createContext({corporateId: null, corporateRole: null,
corporateAdmin: null, corporateSuperAdmin: null});
const UpdateCorporateContext = React.createContext({});
export const useCorporateContext = () => {
return useContext(CorporateContext);
};
export const useUpdateCorporateContext = () => {
return useContext(UpdateCorporateContext);
};
export const CorporateProvider = ({ value, children }) => {
const [details, setDetails] = useState(value);
return (
<CorporateContext.Provider value={details}>
<UpdateCorporateContext.Provider value={setDetails}>
{children}
</UpdateCorporateContext.Provider>
</CorporateContext.Provider>
);
};
export default CorporateProvider;
Here is how I try to fetch context value from child component which is wrapped under wrapper component:
const { corporateId } = useCorporateContext();
I've created a react function component for the context as follows:
const ItemContext = createContext()
const ItemProvider = (props) => {
const [item, setItem] = useState(null)
const findById = (args = {}) => {
fetch('http://....', { method: 'POST' })
.then((newItem) => {
setItem(newItem)
})
}
let value = {
actions: {
findById
},
state: {
item
}
}
return <ItemContext.Provider value={value}>
{props.children}
</ItemContext.Provider>
}
In this way, I have my context that handles all the API calls and stores the state for that item. (Similar to redux and others)
Then in my child component further down the line that uses the above context...
const smallComponent = () =>{
const {id } = useParams()
const itemContext = useContext(ItemContext)
useEffect(()=>{
itemContext.actions.findById(id)
},[id])
return <div>info here</div>
}
So the component should do an API call on change of id. But I'm getting this error in the console:
React Hook useEffect has a missing dependency: 'itemContext.actions'. Either include it or remove the dependency array react-hooks/exhaustive-deps
If I add it in the dependency array though, I get a never ending loop of API calls on my server. So I'm not sure what to do. Or if I'm going at this the wrong way. Thanks.
=== UPDATE ====
Here is a jsfiddle to try it out: https://jsfiddle.net/zx5t76w2/
(FYI I realized the warning is not in the console as it's not linting)
You could just utilize useCallback for your fetch method, which returns a memoized function:
const findById = useCallback((args = {}) => {
fetch("http://....", { method: "POST" }).then(newItem => {
setItem(newItem);
});
}, []);
...and put it in the useEffect:
...
const { actions, state } = useContext(ItemContext)
useEffect(() => {
actions.findById(id)
}, [id, actions.findById])
...
Working example: https://jsfiddle.net/6r5jx1h7/1/
Your problem is related to useEffect calling your custom hook again and again, because it's a normal function that React is not "saving" throughout the renders.
UPDATE
My initial answer fixed the infinite loop.
Your problem was also related to the way you use the context, as it recreates the domain objects of your context (actions, state, ..) again and again (See caveats in the official documentation).
Here is your example in Kent C. Dodds' wonderful way of splitting up context into state and dispatch, which I can't recommend enough. This will fix your infinite loop and provides a cleaner structure of the context usage. Note that I'm still using useCallback for the fetch function based on my original answer:
Complete Codesandbox https://codesandbox.io/s/fancy-sea-bw70b
App.js
import React, { useEffect, useCallback } from "react";
import "./styles.css";
import { useItemState, ItemProvider, useItemDispatch } from "./item-context";
const SmallComponent = () => {
const id = 5;
const { username } = useItemState();
const dispatch = useItemDispatch();
const fetchUsername = useCallback(async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users/" + id
);
const user = await response.json();
dispatch({ type: "setUsername", usernameUpdated: user.name });
}, [dispatch]);
useEffect(() => {
fetchUsername();
}, [fetchUsername]);
return (
<div>
<h4>Username from fetch:</h4>
<p>{username || "not set"}</p>
</div>
);
};
export default function App() {
return (
<div className="App">
<ItemProvider>
<SmallComponent />
</ItemProvider>
</div>
);
}
item-context.js
import React from "react";
const ItemStateContext = React.createContext();
const ItemDispatchContext = React.createContext();
function itemReducer(state, action) {
switch (action.type) {
case "setUsername": {
return { ...state, username: action.usernameUpdated };
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
function ItemProvider({ children }) {
const [state, dispatch] = React.useReducer(itemReducer, {
username: "initial username"
});
return (
<ItemStateContext.Provider value={state}>
<ItemDispatchContext.Provider value={dispatch}>
{children}
</ItemDispatchContext.Provider>
</ItemStateContext.Provider>
);
}
function useItemState() {
const context = React.useContext(ItemStateContext);
if (context === undefined) {
throw new Error("useItemState must be used within a CountProvider");
}
return context;
}
function useItemDispatch() {
const context = React.useContext(ItemDispatchContext);
if (context === undefined) {
throw new Error("useItemDispatch must be used within a CountProvider");
}
return context;
}
export { ItemProvider, useItemState, useItemDispatch };
Both of these blog posts helped me a lot when I started using context with hooks initially:
https://kentcdodds.com/blog/application-state-management-with-react
https://kentcdodds.com/blog/how-to-use-react-context-effectively
OK, I didn't want to write an answer as Bennett basically gave you the fix, but I think it is missing the part in the component, so here you go:
const ItemProvider = ({ children }) => {
const [item, setItem] = useState(null)
const findById = useCallback((args = {}) => {
fetch('http://....', { method: 'POST' }).then((newItem) => setItem(newItem))
}, []);
return (
<ItemContext.Provider value={{ actions: { findById }, state: { item } }}>
{children}
</ItemContext.Provider>
)
}
const smallComponent = () => {
const { id } = useParams()
const { actions } = useContext(ItemContext)
useEffect(() => {
itemContext.actions.findById(id)
}, [actions.findById, id])
return <div>info here</div>
}
Extended from the comments, here's the working JSFiddle
I'm trying to create a random quote machine with react hooks and I can code a list of them to appear, but I'm confused as to how to add steps to the useEffect function to get only one random one to show up.
I was able to list out each quote by mapping over them, but I don't know where in useEffect to have it pick only one quote and display it. I know how to code it for a class component, but how would I update this to reflect react hooks.
class RandomQuote extends Component {
constructor(props) {
super(props)
this.state = {
quote: '',
author: ''
}
}
componentDidMount() {
this.getQuote()
}
getQuote() {
let url =
'https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json'
axios.get(url)
.then(res => {
let data = res.data.quotes
let quoteNum = Math.floor(Math.random() * data.length)
let randomQuote = data[quoteNum]
this.setState({
quote: randomQuote['quote'],
author: randomQuote['author']
})
})
}
getNewQuote = () => { //will be called on clicking the New Quote button
this.getQuote()
}
const [data, setData] = useState({ quotes: [
{quote: '', author: ''}
] });
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json',
);
setData(result.data);
};
fetchData();
}, []);
I just need help 'translating' from and old React to the new hooks.
This is the full "translation" to React Hooks:
import { useCallback, useEffect, useState } from "react";
const RandomQuote = props => {
const [quote, setQuote] = useState("");
const [author, setAuthor] = useState("");
function getQuote() {
const url =
"https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json";
axios.get(url).then(res => {
const data = res.data.quotes;
const quoteNum = Math.floor(Math.random() * data.length);
const randomQuote = data[quoteNum];
setQuote(randomQuote.quote);
setAuthor(randomQuote.author);
});
}
useEffect(() => {
getQuote();
}, []);
const onButtonClick = useCallback(event => {
getQuote();
}, []);
return <button onClick={onButtonClick} />;
};