I am using react-apollo to query the graphQL server and able to successfully hydrate the client with the data. As there will be more than a single place I will be querying for the data I am trying to create a container (refactor) to encapsulate the useQuery hook so that it can be used in one place.
First Try ( working as expected )
const HomeContainer = () => {
const { data, error, loading } = useQuery(GET_DATA_QUERY, {
variables: DATA_VARIABLES
});
const [transformedData, setTransformedData] = useState();
useEffect(() => {
if(!!data) {
const transformedData = someTransformationFunc(data);
setTransformedData(...{transformedData});
}
}, [data]);
if (loading) {
return <div>Loading data ...</div>;
}
if (error) {
return <p>Error loading data</p>;
}
if (!data) {
return <p>Not found</p>;
}
return <Home transformedData={transformedData} />;
};
I wanted to encapsulate the ceremony around different stages of the query to a new container ( loading, error state) so that I can reduce code duplication.
First stab at refactoring
The Query container gets passed in the query, variables and the callback. This takes the responsibility of returning different nodes based on the state of the query ( loading, error or when no data comes back ).
const HomeContainer = () => {
const {data, error, loading} = useQuery(GET_DATA_QUERY, {
variables: DATA_VARIABLES
});
const [transformedData, setTransformedData] = useState();
const callback = (data) => {
const transformedData = someTransformationFunc(data);
setTransformedData(...{
transformedData
});
};
return (
<QueryContainer
query={GET_DATA_QUERY}
variables={DATA_VARIABLES}
callback ={callback}
>
<Home transformedData={transformedData} />
</QueryContainer>
)
};
const QueryContainer = ({callback, query, variables, children }) => {
const {data, error, loading } = useQuery(query, {
variables: variables
});
// Once data is updated invoke the callback
// The transformation of the raw data is handled by the child
useEffect(() => {
if (!!data) {
callback(data);
}
}, [data]);
if (loading) {
return <div > Loading data... < /div>;
}
if (error) {
return <p > Error loading data < /p>;
}
if (!data) {
return <p > Not found < /p>;
}
return children;
};
QueryContainer is using useEffect and invokes the callback when data comes back. I felt this is a bit messy and defeats the purpose of encapsulating in the parent and using the callback to talk and update the child.
Third Try ( Using children as function )
Got rid of the callback and passing the data as the first argument to the children function.
const HomeContainer = () => {
return (
<QueryContainer
query={GET_DATA_QUERY}
variables={DATA_VARIABLES}
>
{(data) => {
const transformedData = someTransformationFunc(data);
return <Home transformedData={transformedData} />;
}}
</QueryContainer>
)
};
const QueryContainer = ({ query, variables, children }) => {
const { data, error, loading } = useQuery(query, {
variables: variables
});
if (loading) {
return <div>Loading data ...</div>;
}
if (error) {
return <p>Error loading data</p>;
}
if (!data) {
return <p>Not found</p>;
}
return children(data);
};
I expected this to work as nothing really changed and the new render when the data is updated calls the children as a function with data as argument.
But when I navigate to that route I see a black screen ( no errors and I can see the correct data logged into the console )
If I click the link again I can see the component committed to the DOM.
Not really sure what is going on here and wondering if someone can throw light as to what is going on here.
hmmm, should work ...
Try something like this (component injection, a bit like HOC - inspiration) :
const HomeContainer = () => {
return (
<QueryContainer
query={GET_DATA_QUERY}
variables={DATA_VARIABLES}
transformation={someTransformationFunc}
renderedComponent={Home}
/>
)
};
const QueryContainer = ({ query, variables, transformation, renderedComponent: View }) => {
const { data, error, loading } = useQuery(query, { variables });
if (loading) {
return <div>Loading data ...</div>;
}
if (error) {
return <p>Error loading data</p>;
}
if (!data) {
return <p>Not found</p>;
}
// let transformedData = transformation(data);
// return <View transformedData={transformedData} />;
return <View transformedData={transformation ? transformation(data) : data} />;
};
If still not working (!?), pass both data and transformation as props and use them to initialize state (with useState or useEffect).
Do you really/still need <HomeContainer/> as an abstraction? ;)
The code snippets that I have added above is working as expected in isolation.
https://codesandbox.io/s/weathered-currying-4ohh3
The problem was with some other component down the hierarchy tree that was causing the component not to re render.
The 2nd implementation is working as expected as the component is getting rendered again dud to the callback being invoked from the parent.
Related
I am trying to make a component that renders "children" prop only "and only if" a boolean is true, now i noticed if i do something like this
const QueryLoader = (props) => {
if (!props.isSuccess) return <h2>Loading</h2>;
return props.children;
};
and use it as follows
const Main = (props) => {
const {isSuccess,data} = fetcher("api");
return (
<QueryLoader isSuccess={isSuccess}>
<div>{data.arrayOfData.innerSomething}</div>
</QueryLoader>
);
};
the data.arrayOfData.innerSomething is still triggered which is causing me issues, i thought about instead of sending children i send a component as a function and then call it inside the QueryLoader but i dont know if this has any side-effects.
Any suggestions?
This is called render prop pattern:
const QueryLoader = ({ isSuccess, children }) => {
return isSuccess ? children() : <h2>Loading</h2>;
};
const Main = () => {
const { isSuccess, data } = fetcher("api");
return (
<QueryLoader isSuccess={isSuccess}>
{() => <div>{data.arrayOfData.innerSomething}</div>}
</QueryLoader>
);
};
For data fetching I hightly recommend using react-query library.
I'm trying to get with nearbySearch from Google Maps API the places that are near to me and send this data to my child component for put markers in map.
This is the function where I call Google Service Nearby Search and it prints the correct data in the console.log
const handleZoomChanged = () => {
let lat = map.mapUrl.split('=')[1].split('&')[0].split(',')[0];
let lng = map.mapUrl.split('=')[1].split('&')[0].split(',')[1];
let places = getNearby(lat, lng);
console.log('places', places);
console.log('length', places.length); //EDIT: I tried to get the length of the array and get 0
return (
<MainMarkersGoogle places={places} />
)
}
But the return it seems not to call the child component and send the data.
I call this function in parent component:
export const MainMap = () => {
const renderMap = () => {
return(
<GoogleMap
id="map"
onLoad={map => {setMap(map)}}
zoom={15}
center={center}
onZoomChanged={handleZoomChanged}>
</GoogleMap>
)
}
return isLoaded ? renderMap() : 'Loading...'
}
This is the child component:
/* global google */
export const MainMarkersGoogle = ( places ) => {
const [markers, setMarkers] = useState([]);
useEffect(() => {
console.log('child', places)
setMarkers(places)
}, [places])
return(
{markers.map(({ id, geometry, name }) => (
<Marker
key={id}
position={geometry.location}
animation={google.maps.Animation.DROP}
onClick={() => console.log(id) }
title={name}
>
</Marker>
))}
)
}
The console.log from useEffect don't appear and markers are not shown in the map.
I tried to receive the child component with
export const MainMarkersGoogle = ({places}) => {
and
export const MainMarkersGoogle = places => {
getting the same result.
EDIT: I show the function getNearby too, maybe the problem is inside this function but i can't find where:
function getNearby(lat, lng) {
let places;
var config = {
method: 'get',
url: `MY_URL`,
headers: { }
};
axios(config)
.then(function (response) {
console.log(response.data);
for(let i = 0; i < response.data.results.length; i++) {
if(response.data.results[i].business_status === 'OPERATIONAL')
places.push(response.data.results[i])
}
})
.catch(function (error) {
console.log(error);
});
return places;
}
If you need more info of my code, ask me and I will edit this.
Check the docs of Google Maps for react, onZoomChanged is a void event listener. It is not supposed to return an element https://react-google-maps-api-docs.netlify.app . One way to do this would be to update a state variable with the places and have the component already mounted. Of course you can big-brain this and find other ways to achieve the same result. Also you are updating places inside a Promise and return places outside of the Promise so the moment you return them they are empty because the Promise is not resolved. Try to use async-await to achieve a blocking-like effect.
I am new to Apollo Client and want to implement pagination. My code looks like this:
I am using RickandMorty endpoint for this (https://rickandmortyapi.com/graphql)
useCharacters.tsx
import { useQuery, gql } from '#apollo/client';
const GET_ALL_CHARACTERS = gql`
query GetCharacters($page: Int) {
characters(page: $page) {
info {
count
pages
}
results {
id
name
}
}
}
`;
export const useCharacters = (page: number = 1) => {
const { data, loading, error } = useQuery(GET_ALL_CHARACTERS, { variables: { page } });
return { data, loading, error };
};
App.tsx
export const App = () => {
const { data, loading, error } = useCharacters(1);
const nextPage = () => {
const { data, loading, error } = useCharacters(2);
};
return (
<>
{loading ? (
<div> Loading... </div>
) : error ? (
<div>Error</div>
) : (
<>
<CharacterList data={data.characters.results} />
<div onClick={nextPage}> Next </div>
</>
);
};
It is fetching data properly the first time but I want to fetch new data when Next button is clicked on page 2.
I know I can't call useQuery() in a method like this as hooks cannot be called inside a block and also the data, error, and loading won't be accessible outside.
How can I fix this issue? I tried googling it but could not find any help related to this.
This might help other developers who are new to Apollo Client and will save them time.
fetchMore() can be used for pagination with Apollo Client.
useCharacters.tsx
export const useCharacters = (page: number = 1, name: string = '') => {
const { data, loading, error, fetchMore } = useQuery(GET_ALL_CHARACTERS, {
variables: { page, name },
notifyOnNetworkStatusChange: true, // to show loader
});
return { data, loading, error, fetchMore }; // returning fetchMore
};
App.tsx
export const App = () => {
const { data, loading, error, fetchMore } = useCharacters(1);
const nextPage = () => {
/* You can call the returned fetchMore() here and pass the next page number.
updateQuery() simply updates your data to the newly fetched records otherwise return previous records
*/
fetchMore({
variables: {
page: 2,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return fetchMoreResult;
},
});
};
return (
<>
{loading ? (
<div> Loading... </div>
) : error ? (
<div>Error</div>
) : (
<>
<CharacterList data={data.characters.results} />
<div onClick={nextPage}> Next </div>
</>
);
};
I like the way in AngularJS of fetching external data before showing webpage. The data will be sent one by one to the frontend before showing the webpage. We are certain that the website and the data on it is good when we see it.
$stateProvider
.state('kpi', {
url:'/kpi',
templateUrl: '/htmls/kpi.html',
resolve: {
getUser: ['lazy', 'auth', function (lazy, auth) { return auth.getUser() }],
postPromise: ['posts', 'getUser', function (posts, getUser) { return posts.getAll() }],
userPromise: ['users', 'postPromise', function (users, postPromise) { return users.getAll() }],
logs: ['kpiService', 'userPromise', function (kpiService, userPromise) { return kpiService.getLogs() }],
subscribers: ['kpiService', 'logs', function (kpiService, logs) { return kpiService.getSubscribers() }]
},
controller: 'KpiCtrl'
})
Now, I would like to achieve this in ReactJS, I tried:
class Kpi extends React.Component {
state = { logs: [] };
getChartOptions1() {
// this.state.logs is used
}
componentDidMount() {
axios.get(`${BACKEND_URL}/httpOnly/kpi/logs`).then(
logs => {
this.setState({ logs.data });
});
};
render() {
return;
<div>
<HighchartsReact
highcharts={Highcharts}
options={this.getChartOptions1()}
{...this.props}
/>
<div>{JSON.stringify(this.state.logs)}</div>
</div>;
}
}
But it seems that it first called getChartOptions1 with unready data, rendered the webpage, then fetched the external data, then called again getChartOptions1 with ready data, rendered the webpage again.
I don't like the fact that getChartOptions was called twice (first with unready data), and the page was rendered twice.
There are several ways discussed: Hooks, React.Suspense, React.Lazy, etc. Does anyone know what's the standard way of fetching external data before showing the webpage in React?
As suggested in comments, conditional rendering might look like
class Kpi extends React.Component {
state = { logs: [] };
getChartOptions1 () {
// this.state.logs is used
}
componentDidMount() {
axios.get(`${BACKEND_URL}/httpOnly/kpi/logs`).then(
logs => {
this.setState({logs.data});
});
};
render() {
return this.state.logs.length ?
(
<div>
<HighchartsReact highcharts={Highcharts} options={this.getChartOptions1()} {...this.props} />
<div>{JSON.stringify(this.state.logs)}</div>
</div>
)
: (<div>'Loading'</div>);
}
}
but it might be better to start with logs: null, in case the fetch returns an empty array
class Kpi extends React.Component {
state = { logs: null };
getChartOptions1 () {
// this.state.logs is used
}
componentDidMount() {
axios.get(`${BACKEND_URL}/httpOnly/kpi/logs`).then(
logs => {
this.setState({logs.data});
});
};
render() {
return this.state.logs ?
(
<div>
<HighchartsReact highcharts={Highcharts} options={this.getChartOptions1()} {...this.props} />
<div>{JSON.stringify(this.state.logs)}</div>
</div>
)
: (<div>'Loading'</div>);
}
}
Like mentioned above, the answer to your problem is is conditional rendering in the Kpi component. But you can take it one step further and make the conditional render directly on the chartoptions since they are the ones you need at the end.
You could also write Kpi as a functional component and go for a hook-solution. And by doing so de-coupling the data fetching from your Kpi component.
You can also make the hook more generic to handle different endpoints so the hook can be reused in multiple places in your app.
export const useBackendData = (initialEndpoint) => {
const [endpoint, setEndpoint] = useState(initialEndpoint);
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(endpoint);
setData(response.data);
}catch(e){
// error handling...
}
}
fetchData();
}, [endpoint])
return [data, setEndpoint]
}
const Kpi = (props) => {
[chartOptions, setChartOptions] = useState(null);
[logs] = useBackendData(`${BACKEND_URL}/httpOnly/kpi/logs`);
const getChartOptions1 = () => {
// do stuff with logs...
setChartOptions([PROCESSED LOG DATA]);
}
useEffect(() => {
if(!!logs.length)
setChartOptions(getChartOptions1())
},[logs]);
return !!chartOptions ? (
<div>
<HighchartsReact highcharts={Highcharts} options={chartOptions} {...props} />
<div>{JSON.stringify(logs)}</div>
</div>) : (
<div>Loading data...</div>
);
}
React lifecycle is like this - first, the render method and then the componentDidMount is called (as per your case) during mounting of components, so you are having this issue.
What I do is show a loader, spinner(anything) till the data is being fetched, and once the data is there, show the actual component. The app needs to do something till it gets the data.
So I had a typescript-react component called ContractExpenses and I am using a JSX component inside it called DynamicSelector, however I get an error as follows
This is the code for my <DynamicSelector/> component
const DynamicSelector = ({ query, variables, ...staticSelectorProps }) => {
const { data, loading, error } = useQuery(query, { variables });
if (error) return null;
let list = [];
if (data) {
const key = Object.keys(data)[0];
list = get(data, key, []);
}
return (
<StaticSelector loading={loading} items={list} {...staticSelectorProps} />
);
};
export default DynamicSelector;
Is there any way, I can avoid such errors, it is killing me to use typescript because i have to convert all the children (and their children!) to typescript
You can make your variables prop optional like, give some default value to it.
const DynamicSelector = ({ query, variables = {}, ...staticSelectorProps }) => {
const { data, loading, error } = useQuery(query, { variables });
if (error) return null;
let list = [];
if (data) {
const key = Object.keys(data)[0];
list = get(data, key, []);
}
return (
<StaticSelector loading={loading} items={list} {...staticSelectorProps} />
);
};
export default DynamicSelector;
Because Typescript expects you to pass variables Prop , either pass it or make it some default or make it optional by making an interface.
Like this
export interface DynamicSelectorProps {
query: any;
variables?: any;
}
const DynamicSelector: FC<DynamicSelectorProps> = ({ query, variables, ...staticSelectorProps }) => {
const { data, loading, error } = useQuery(query, { variables });
if (error) return null;
let list = [];
if (data) {
const key = Object.keys(data)[0];
list = get(data, key, []);
}
return (
<StaticSelector loading={loading} items={list} {...staticSelectorProps} />
);
};
export default DynamicSelector;
Edit
passing undefined as the default value for props is better since, it will be ignored by the component most of the time, resulting in fewer bugs, if any.