Standard way of fetching external data before showing webpage (2021 September) - reactjs

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.

Related

Redux toolkit mutation access to state across multiple components

I'm start using RTQ.
To access query data is possible via generated hook. But for mutations, it doesn't work.
I have this simple example.
This component is responsible for loading "users" from API.
const Page = () => {
const {
isSuccess: isSuccessUsers
isLoading: isLoadingUsers
} = useGetUsersQuery({})
if (isLoadingUsers) return <LoadingSpinner />
if (isSuccessUsers) return <UsersTable />
...
}
This component display users, which are obtained from cache - was filled by the API call in previous component "Page".
Also in that component I want display loading spinner for deleting state.
const UsersTable = () => {
const {
data: dataUsers,
isFetching: isFetchingUsers,
} = useGetUsersQuery({})
const [, {
data: dataDeleteUser,
isLoading: isLoadingDeleteUser,
isSuccess: isSuccessDeleteUser,
isError: isErrorDeleteUser,
error: errorDeleteUser,
}] = useDeleteUserMutation()
useEffect(() => {
if (isSuccess) {
console.log(data.data.message)
}
}, [isSuccess])
useEffect(() => {
if (isError) {
console.log(error.data.message)
}
}, [isError])
if (usersIsFetching || isLoadingDeleteUser) return <LoadingSpinner />
dataUsers.map(user=>(<UsersTableRow user={user}/>))
}
In this component I only want call delete function. But here, when use same pattern just like for query, it doesnt work.
const UsersTableRow = ({user}) => {
const [deleteUser] = useDeleteUserMutation()
const handleDeleUser = () => {
deleteUser(user.id)
}
...
}
I could solve this by pass deleteUser function as prop from UsersTable component to UsersTableRow component.
const [ deleteUser, {
data: dataDeleteUser,
isLoading: isLoadingDeleteUser,
isSuccess: isSuccessDeleteUser,
isError: isErrorDeleteUser,
error: errorDeleteUser,
}] = useDeleteUserMutation()
dataUsers.map(user=>(<UsersTableRow deleteUser={deleteUser} user={user}/>))
I would like to avoid this, because if there are more child components, I will have to pass to every child.
Is there some better solution?
You can use a fixed cache key to "tell" redux-toolkit that you want to use the same result for both components:
export const ComponentOne = () => {
// Triggering `deleteUser` will affect the result in both this component,
// but as well as the result in `ComponentTwo`, and vice-versa
const [deleteUser, result] = useDeleteUserMutation({
fixedCacheKey: 'shared-delete-user',
})
return <div>...</div>
}
export const ComponentTwo = () => {
const [deleteUser, result] = useDeleteUserMutation({
fixedCacheKey: 'shared-delete-user',
})
return <div>...</div>
}

Child component not being updated with funcional components

I have a component where I get repositories like below:
useEffect(() => {
loadRepositories();
}, []);
const loadRepositories = async () => {
const response = await fetch('https://api.github.com/users/mcand/repos');
const data = await response.json();
setUserRepositories(data);
};
return (
<Repository repositories={userRepositories} />
)
The child component only receives the repositories and prints them. It's like that:
const Repository = ({ repositories }) => {
console.log('repository rendering');
console.log(repositories);
const [repos, setRepos] = useState(repositories);
useEffect(() => {
setRepos(repositories);
}, [ ]);
const getRepos = () => {
repos.map((rep, idx) => {
return <div key={idx}>{rep.name}</div>;
});
};
return (
<>
<RepositoryContainer>
<h2>Repos</h2>
{getRepos()}
</>
);
};
export default Repository;
The problem is that, nothing is being displayed. In the console.log I can see that there're repositories, but it seems like the component cannot update itself, I don't know why. Maybe I'm missing something in this useEffect.
Since you're putting the repository data from props into state, then then rendering based on state, when the props change, the state of a Repository doesn't change, so no change is rendered.
The repository data is completely handled by the parent element, so the child shouldn't use any state - just use the prop from the parent. You also need to return from the getRepos function.
const Repository = ({ repositories }) => {
const getRepos = () => {
return repositories.map((rep, idx) => {
return <div key={idx}>{rep.name}</div>;
});
};
return (
<>
<RepositoryContainer>
<h2>Repos</h2>
{getRepos()}
</>
);
};
export default Repository;
You can also simplify
useEffect(() => {
loadRepositories();
}, []);
to
useEffect(loadRepositories, []);
It's not a bug yet, but it could be pretty misleading - your child component is named Repository, yet it renders multiple repositories. It might be less prone to cause confusion if you named it Repositories instead, or have the parent do the .map instead (so that a Repository actually corresponds to one repository array item object).
Change this
useEffect(() => {
setRepos(repositories);
}, [ ]);
to
useEffect(() => {
setRepos(repositories);
}, [repositories]);

Component not rendering when using children as function

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.

Component won't re-render on state change, even though console.log in render function is called

I'm sure I'm doing something silly here. I'm trying to write a component which, based on the fetching state of the data, renders one of 3 things.
1) An error message if the fetch has errored.
2) A loading message if the data is fetching.
3) The full child component if the data is fetched.
What's happening now is the fetch succeeds, but the Loading message won't disappear. In the code example below, you can see that I've added a console.log for whether or not the full render should occur. What's perplexing me is that this will eventually log as true, but the Loading message still shows.
This exact pattern seems to work for a sibling component in this app, so I'm confused as to what's missing here... Here's the entire component, it's been anonymized and stripped of styles, but the structure is otherwise identical.
function mapStateToProps(state) {
return {
Data: state.NewDataReducer,
};
}
const mapDispatchToProps = {
fetchNewData: ActionCreators.fetchNewData,
};
const DataError = () => (
<div>
Error fetching new data!
</div>
);
const DataLoading = () => (
<div>
Loading..
</div>
);
class MainContainer extends PureComponent {
static propTypes = {
choice: PropTypes.string,
fetchNewData: PropTypes.func,
Data: ImmutablePropTypes.map.isRequired,
}
state = {
choice: null,
}
componentDidUpdate(nextProps) {
const { choice, fetchNewData, Data } = this.props;
if(!choice) {
return;
}
if(isFetching(Data)) {
return;
}
const newChoiceSelected = choice !== nextProps.choice;
if(newChoiceSelected) {
fetchNewData({choice});
}
}
handleChangeChoice = (choice) => {
this.setState({
choice: { choice }
});
}
render() {
const { choice, Data } = this.props;
const error = hasFetchError(Data);
const loading = !error && !isFetched(Data);
const renderFull = !loading && !error;
console.log(renderFull);
if(!renderFull) {
return (
<div>
Please wait.
</div>
);
}
const { dataBreakdown } = Data.get("dataKey").toJS();
return (
<div>
<ChildComponent
choice={choice}
dataBreakdown={dataBreakdown}
onSetDrillDown={this.handleChangeChoice}
/>
</div>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(MainContainer);
You need to pass your map function the arguments of what it needs to map over from immutable data types. Map creates a new a array and makes a callback function on the items that it iterates through in that array. This is just an example
Data: ImmutabaleProptypes.map((ing, index)=>(
<li key={index}>{Data}</li> // you can write jsx in here.
));

What is the proper way of changing states in react for loading animations from the beginning of a GET request to the end of it?

I want to be able to trigger a load animation in React and perform a GET request, and then stop the loading animation after the get request completed. This is how I laid out my code.
export default class Dialog extends Component {
state = {
domIdx: 0
}
loadData = () => {
this.setState({
domIdex: 1
}, $.getJSON('http://google.com', () => {
this.setState({
domIdx: 2
})
})
}
render() {
let arr = [<UploadFile/>, <LoadAnimation/>, <Done/>]
return (
<div>
{arr[this.state.domIdx]}
</div>
)
}
}
However, the above code is not working after the loading animation is triggered. The loading animation is shown, the GET request is completed, but the view doesn't change after calling setState again.
How can I achieve the intended action?
In your component state, you can define a loading piece of state:
state = { loading: false }
Then, when you make your request, flip it to to true:
loadData = () => {
this.setState({
loading: true
}, $.getJSON('http://google.com', () => {
this.setState({
loading: false
})
})
}
Then in your render:
render() {
return (
<div>
{this.state.loading ? (<SomeSpinner />):(<Completed />)}
</div>
)
}

Resources