I've been following a great course on how to build a server-side rendered app with React and Redux, but I'm now in a situation that the course doesn't cover and I can't figure out by myself.
Please consider the following component (it's pretty basic, except for the export part at the bottom):
class HomePage extends React.Component {
componentDidMount() {
this.props.fetchHomePageData();
}
handleLoadMoreClick() {
this.props.fetchNextHomePagePosts();
}
render() {
const posts = this.props.posts.homepagePosts;
const featuredProject = this.props.posts.featuredProject;
const featuredNews = this.props.posts.featuredNews;
const banner = this.props.posts.banner;
const data = ( posts && featuredProject && featuredNews && banner );
if( data == undefined ) {
return <Loading />;
}
return(
<div>
<FeaturedProject featuredProject={ featuredProject } />
<FeaturedNews featuredNews={ featuredNews } />
<Banner banner={ banner } />
<PostsList posts={ posts } heading="Recently on FotoRoom" hasSelect={ true } />
<LoadMoreBtn onClick={ this.handleLoadMoreClick.bind( this ) } />
</div>
);
}
}
function mapStateToProps( { posts } ) {
return { posts }
}
export default {
component: connect( mapStateToProps, { fetchHomePageData, fetchNextHomePagePosts } )( HomePage ),
loadData: ( { dispatch } ) => dispatch( fetchHomePageData() )
};
The above works fine: the loadData function makes an API request to fetch some data, which is fed into the component through the mapStateToProps function. But what if I wanted to fire multiple action creators in that same loadData function? The only thing that kind of works is if I write the function like this:
function loadData( store ) {
store.dispatch( fetchFeaturedNews() );
return store.dispatch( fetchHomePageData() );
}
export default {
component: connect( mapStateToProps, { fetchHomePageData, fetchNextHomePagePosts } )( HomePage ),
loadData: loadData
};
but this is not great because I need all data to be returned... Keep in mind that the exported Component ends up in the following route configuration:
const Routes = [
{
...App,
routes: [
{
...HomePage, // Here it is!
path: '/',
exact: true
},
{
...LoginPage,
path: '/login'
},
{
...SinglePostPage,
path: '/:slug'
},
{
...ArchivePage,
path: '/tag/:tag'
},
]
}
];
and here's how the loadData function is used once the component is needed by a certain route:
app.get( '*', ( req, res ) => {
const store = createStore( req );
const fetchedAuthCookie = req.universalCookies.get( authCookie );
const promises = matchRoutes( Routes, req.path ).map( ( { route } ) => {
return route.loadData ? route.loadData( store, req.path, fetchedAuthCookie ) : null;
}).map( promise => {
if( promise ) {
return new Promise( ( resolve, reject ) => {
promise.then( resolve ).catch( resolve );
});
}
});
...
}
Also, here's an example of the actions fired by the action creators. They all return promises:
export const fetchHomePageData = () => async ( dispatch, getState, api ) => {
const posts = await api.get( allPostsEP );
dispatch({
type: 'FETCH_POSTS_LIST',
payload: posts
});
}
and the reducer:
export default ( state = {}, action ) => {
switch( action.type ) {
case 'FETCH_POSTS_LIST':
return {
...state,
homepagePosts: action.payload.data
}
default:
return state;
}
}
So your actions return a Promise, and you are asking how can you return more than one Promise. Use Promise.all:
function loadData({ dispatch }) {
return Promise.all([
dispatch( fetchFeaturedNews() ),
dispatch( fetchHomePageData() ),
]);
}
But... remember that Promise.all will resolve when all of it's Promises resolve, and it will return an Array of values:
function loadData({ dispatch }) {
return Promise.all([
dispatch( fetchFeaturedNews() ),
dispatch( fetchHomePageData() ),
]).then(listOfResults => {
console.log(Array.isArray(listOfResults)); // "true"
console.log(listOfResults.length); // 2
});
}
So you will probably want to handle it differently.
Related
Okay so I am using Redux and Axios to post data to my server and subsequently rendering the server response, error or otherwise, in my component. There are multiple post requests' response that fill up the Redux store and I am trying to render those responses whenever one is triggered.
I am getting the response from the server in my component like this:
const createdPost = useSelector( state => state.createPost );
const { loading: createdPostLoading, error: createdPostError, success: createdPostSuccess } = createdPost;
const uploadedImage = useSelector( state => state.uploadImage );
const { loading: uploadedImageLoading, error: uploadedImageError, success: uploadedImageSuccess } = uploadedImage;
const deletedImage = useSelector( state => state.deleteImage);
const { loading: deletedImageLoading, error: deletedImageError, success: deletedImageSuccess } = deletedImage;
So in my useEffect I'm checking their values and rendering them, like this:
const [ responseMessage, setResponseMessage ] = useState( '' );
useEffect( () => {
if ( createdPostError ) {
setResponseMessage( createdPostError );
} else if ( createdPostSuccess ) {
setResponseMessage( createdPostSuccess );
} else if ( uploadedImageError ) {
setResponseMessage( uploadedImageError );
} else if ( uploadedImageSuccess ) {
setResponseMessage( uploadedImageSuccess );
} else if ( deletedImageError ) {
setResponseMessage( deletedImageError );
} else if ( deletedImageSuccess ) {
setResponseMessage( deletedImageSuccess );
}
}, [ createdPostError, createdPostSuccess, uploadedImageError, uploadedImageSuccess, deletedImageError, deletedImageSuccess ] );
And the render function look like this:
{
<Message type='Error' text={responseMessage} />
}
The issue right now is whenever I try to delete an image and the server fails to delete it, I get the uploadedImageSuccess response, which I think I'm getting from the store where it is already populated with previously uploaded image response from the server. I am supposed to get the delete image response. If I log the value of deletedImageError then I can see the actual server response. It doesn't render it.
The conditional statement in useEffect for the uploadedImageSuccesss is being triggered. So I am guessing this is not the proper way of handling error. Anybody know a good way of handling them? Where am I going wrong here?
UPDATE
Here's the full code:
Redux:
=================
CONSTANTS.js:
=================
export const CREATE_POST_REQUEST = 'CREATE_POST_REQUEST';
export const CREATE_POST_SUCCESS = 'CREATE_POST_SUCCESS';
export const CREATE_POST_FAIL = 'CREATE_POST_FAIL';
export const UPLOAD_IMAGE_REQUEST = 'UPLOAD_IMAGE_REQUEST';
export const UPLOAD_IMAGE_SUCCESS = 'UPLOAD_IMAGE_SUCCESS';
export const UPLOAD_IMAGE_FAIL = 'UPLOAD_IMAGE_FAIL';
export const DELETE_IMAGE_REQUEST = 'DELETE_IMAGE_REQUEST';
export const DELETE_IMAGE_SUCCESS = 'DELETE_IMAGE_SUCCESS';
export const DELETE_IMAGE_FAIL = 'DELETE_IMAGE_FAIL';
=================
REDUCERjs
=================
export const createPostReducer = ( state = { campaign: {} }, action ) => {
switch ( action.type ) {
case CREATE_POST_REQUEST:
return { loading: true };
case CREATE_POST_SUCCESS:
return {
loading: false,
success: action.payload
};
case CREATE_POST_FAIL:
return {
loading: false,
error: action.payload
};
default:
return state;
}
}
export const uploadImageReducer = ( state = {}, action ) => {
switch ( action.type ) {
case UPLOAD_IMAGE_REQUEST:
return { loading: true };
case UPLOAD_IMAGE_SUCCESS:
return {
loading: false,
success: action.payload
};
case UPLOAD_IMAGE_FAIL:
return {
loading: false,
error: action.payload
};
default:
return state;
}
}
export const deleteImageReducer = ( state = {}, action ) => {
switch ( action.type ) {
case DELETE_IMAGE_REQUEST:
return { loading: true };
case DELETE_IMAGE_SUCCESS:
return {
loading: false,
success: action.payload
};
case DELETE_IMAGE_FAIL:
return {
loading: false,
error: action.payload
};
default:
return state;
}
}
=================
ACTIONS.js
=================
export const createPost = ( postData ) => async ( dispatch ) => {
try {
dispatch({
type: CREATE_POST_REQUEST
});
const { data } = await axios.post( '/api/posts', postData);
dispatch({
type: CREATE_POST_SUCCESS,
payload: data.message
});
} catch ( error ) {
dispatch({
type: CREATE_POST_FAIL,
payload: error.message
});
}
}
export const uploadImage = ( imageData ) => async ( dispatch ) => {
try {
dispatch({
type: UPLOAD_IMAGE_REQUEST
});
const { data } = await axios.post( '/api/posts/upload-image', imageData );
dispatch({
type: UPLOAD_IMAGE_SUCCESS,
payload: data
});
} catch ( error ) {
dispatch({
type: UPLOAD_IMAGE_FAIL,
payload: error.message
});
}
}
export const deleteImage = ( imageData ) => async ( dispatch ) => {
try {
dispatch({
type: DELETE_IMAGE_REQUEST
});
const { data } = await axios.post( '/api/posts/delete-image', imageData );
dispatch({
type: DELETE_IMAGE_SUCCESS,
payload: data
});
} catch ( error ) {
dispatch({
type: DELETE_IMAGE_FAIL,
payload: error.message
});
}
}
=================
STORE.js
=================
const reducer = combineReducers({
createPost: createPostReducer,
uploadImage: uploadImageReducer,
deleteImage: deleteImageReducer
});
const middleware = [ thunk ];
const store = createStore(
reducer,
initialState,
composeWithDevTools( applyMiddleware( ...middleware ) )
);
export default store;
Here's how the component looks like:
const Post = () => {
const [ responseMessage, setResponseMessage ] = useState( '' );
const createdPost = useSelector( state => state.createPost );
const { loading: createdPostLoading, error: createdPostError, success: createdPostSuccess } = createdPost;
const uploadedImage = useSelector( state => state.uploadImage );
const { loading: uploadedImageLoading, error: uploadedImageError, success: uploadedImageSuccess } = uploadedImage;
const deletedImage = useSelector( state => state.deleteImage);
const { loading: deletedImageLoading, error: deletedImageError, success: deletedImageSuccess } = deletedImage;
useEffect( () => {
if ( createdPostError ) {
setResponseMessage( createdPostError );
} else if ( createdPostSuccess ) {
setResponseMessage( createdPostSuccess );
} else if ( uploadedImageError ) {
setResponseMessage( uploadedImageError );
} else if ( uploadedImageSuccess ) {
setResponseMessage( uploadedImageSuccess );
} else if ( deletedImageError ) {
setResponseMessage( deletedImageError );
} else if ( deletedImageSuccess ) {
setResponseMessage( deletedImageSuccess );
}
}, [ createdPostError, createdPostSuccess, uploadedImageError, uploadedImageSuccess, deletedImageError, deletedImageSuccess ] );
return (
{
<Message type='Error' text={responseMessage} />
}
)
}
export default Post;
Create separate Reducer for Generic Error Handler
export const genericErrorReducer=(state,action){
switch(action.type){
case 'GENERIC_ERROR':
return {
error:action.payload
}
}
}
call this when you are getting error from server rather then creating separate local state for each error
I am testing a react component which renders another component which calls an endpoint and returns some data and is displayed, i want to know how i can mock the component that calls the endpoint and return dummy data for each test
This is the component i am testing
class MetaSelect extends React.Component {
render() {
console.log('metaselect render', MetadataValues);
return (
<MetadataValues type={this.props.type}>
{({ items, isLoading }) => (
<>
{isLoading ? (
<Icon variant="loadingSpinner" size={36} />
) : (
<Select {...this.props} items={items} placeholder="Please select a value" />
)}
</>
)}
</MetadataValues>
);
}
}
MetaSelect.propTypes = {
type: PropTypes.string.isRequired
};
I want to mock the MetadataValues in my tests, this is the metadataValues.js file
class MetadataValues extends React.Component {
state = {
items: [],
isLoading: true
};
componentDidMount() {
this.fetchData();
}
fetchData = async () => {
const items = await query(`....`);
this.setState({ items, isLoading: false });
};
render() {
return this.props.children({ items: this.state.items, isLoading: this.state.isLoading });
}
}
MetadataValues.propTypes = {
type: PropTypes.string.isRequired,
children: PropTypes.func.isRequired
};
This is my metaSelect.test.js file
jest.mock('../MetadataValues/MetadataValues');
describe.only('MetaSelect component', () => {
fit('Should display spinner when data isnt yet recieved', async () => {
MetadataValues.mockImplementation( ()=> { <div>Mock</div>});
const wrapper = mount(<MetaSelect type="EmployStatus"/>);
expect( wrapper.find('Icon').exists() ).toBeTruthy();
});
});
Im guessing i need to add something in the MetadataValues.mockImplementation( )
but im not sure what i should add to mock the component correctly
If you only need one version of the mock in your test this should be enough:
jest.mock('../MetadataValues/MetadataValues', ()=> ()=> <div>Mock</div>);
If you need to different mock behaviour you need to mock it like this
import MetadataValues from '../MetadataValues/MetadataValues'
jest.mock('../MetadataValues/MetadataValues', ()=> jest.fn());
it('does something', ()={
MetadataValues.mockImplementation( ()=> { <div>Mock1</div>});
})
it('does something else', ()={
MetadataValues.mockImplementation( ()=> { <div>Mock2</div>});
})
what about using shallow() instead of mount()?
const mockedItems = [{.....}, {...}, ...];
it('shows spinner if data is loading', () => {
const wrapper = shallow(<MetaSelect type={...} /*other props*/ />);
const valuesChildren = wrapper.find(MetadataValues).prop('children');
const renderResult = valuesChildren(mockedItems, true);
expect(renderResult.find(Icon)).toHaveLength(1);
expect(renderResult.find(Icon).props()).toEqual({
variant: "LoadingSpinner", // toMatchSnapshot() may be better here
size: 36,
});
});
This not only makes mocking in natural way but also has some benefits
it('passes type prop down to nested MetadataValues', () => {
const typeMocked = {}; // since it's shallow() incorrect `type` does not break anything
const wrapper = shallow(<MetaSelect type={typeMocked} >);
expect(wrapper.find(MetadataValues).prop('type')).toStrictEqual(typeMocked);
})
I have two more fetching requests in one page, how to arrange them one by one?
Just a code for instance, expecting the fetching queue is executed in num order.
class FetchingQueue extends Component {
...
componentDidUpdate(preProps) {
if ( this.props.prop1 != preProps.props1) {
this.props.fetching1
this.props.fetching2
this.timeoutHanlde = setTimeout(() => {
this.props.fetching3
}, 1000)
}
}
render() {
return (
...
)
}
}
export default connect(
state => ({
prop1: state.reducer.props1
}),
{ fetching1, fetching2, fetching3 }
)(FetchingQueue)
Just return Promises from the fetching functions and then wait for them:
class FetchingQueue extends Component {
...
async componentDidMount() {
const fetching1Result = await this.props.fetching1();
const fetching2Result = await this.props.fetching2();
const fetching3Result = await this.props.fetching3();
}
render() {
return (
...
)
}
}
export default connect(
state => ({ prop1: state.reducer.props1 }),
{ fetching1, fetching2, fetching3 }
)(FetchingQueue)
Fetching function can look like this:
const fetching1 = new Promise((resolve, reject) => {
// call resolve when ready
resolve('result');
});
I've read many examples about this and got no result for my problem, I want to get the values inside MySQL database by using localhost, code with PHP and return the value as JSON format e.g.
[
{"id":"7",
"name":"Sammy",
"address":"New York",
"age":"42"}
]
with this format, I can fetch the data by using this code in GetApi.js
class GetApi {
static getAllUsers() {
return fetch('http://192.168.1.199/App/show_all_data.php')
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
dispatch(itemsIsLoading(false));
return response;
})
.then((response) => response.json())
.then((items) => dispatch(itemsFetchDataSuccess(items)))
.catch(() => dispatch(itemsHasErrored(true)));
}
}
export default GetApi;
here's the action.js
import GetApi from '../../api/GetApi';
export function itemsHasErrored(bool: boolean) {
return {
type: "ITEMS_HAS_ERRORED",
hasErrored: bool
};
}
export function itemsIsLoading(bool: boolean) {
return {
type: "ITEMS_IS_LOADING",
isLoading: bool
};
}
export function itemsFetchDataSuccess(items: Object) {
return {
type: "ITEMS_FETCH_DATA_SUCCESS",
items
};
}
export function itemsFetchData(url: any) {
return function(dispatch) {
return GetApi.getAllUsers().then(items => {
dispatch(itemsFetchDataSuccess(items));
dispatch(itemsIsLoading(false));
}).catch(error => {
throw(error);
});
};
}
here's the reducer.js
const initialState = {
isLoading: true,
hasErrored: false,
items: []
};
export default function(state: any = initialState, action: Function) {
switch (action.type) {
case "ITEMS_HAS_ERRORED":
return { ...state, hasErrored: action.hasErrored };
case "ITEMS_IS_LOADING":
return { ...state, isLoading: action.isLoading };
case "ITEMS_FETCH_DATA_SUCCESS":
return { ...state, items: action.items };
default:
return state;
}
}
called action.js function in index.js
import { itemsFetchData } from "../../actions";
...
all codings that were not related with calling action.js
...
const navigation = this.props.navigation;
let items = this.props.items;
if (items.hasOwnProperty('item')) {
items = items.item
}
return (
<List
dataArray={this.props.items}
renderRow={(
data
) =>
<ListItem icon style={styles.listitem}>
<Left>
<Text>
{data.name}
</Text>
</Left>
<Right>
<Text>
{data.address}
</Text>
</Right>
</ListItem>}
/>
);
function bindAction(dispatch) {
return {
fetchData: url => dispatch(itemsFetchData(url))
};
}
const mapStateToProps = state => ({
items: state.homeReducer.items,
hasErrored: state.homeReducer.hasErrored,
isLoading: state.homeReducer.isLoading
});
export default connect(mapStateToProps, bindAction)(ShowData);
I got no results when I'm running the code, it's just showed the loading icon. even when I set isLoading:false, the home menu showed up without the data
I'm just trying to minimize the code inside index.js because it's too long to post that here. I will do that if necessary in the next comment.
I recommend using epics , below is an example link for you to follow.
Epic Example
You can look at the actions and data ajax calls from epic and how it connects back to the action.
Note: Axios is been used here instead of fetch api...
So I have a large set of data that I'm retrieving from an API. I believe the problem is that my component is calling the renderMarkers function before the data is received from the promise.
So I am wondering how I can wait for the promise to resolve the data completely before calling my renderMarkers function?
class Map extends Component {
componentDidMount() {
console.log(this.props)
new google.maps.Map(this.refs.map, {
zoom: 12,
center: {
lat: this.props.route.lat,
lng: this.props.route.lng
}
})
}
componentWillMount() {
this.props.fetchWells()
}
renderMarkers() {
return this.props.wells.map((wells) => {
console.log(wells)
})
}
render() {
return (
<div id="map" ref="map">
{this.renderMarkers()}
</div>
)
}
}
function mapStateToProps(state) {
return { wells: state.wells.all };
}
export default connect(mapStateToProps, { fetchWells })(Map);
You could do something like this to show a Loader until all the info is fetched:
class Map extends Component {
constructor () {
super()
this.state = { wells: [] }
}
componentDidMount() {
this.props.fetchWells()
.then(res => this.setState({ wells: res.wells }) )
}
render () {
const { wells } = this.state
return wells.length ? this.renderWells() : (
<span>Loading wells...</span>
)
}
}
for functional components with hooks:
function App() {
const [nodes, setNodes] = useState({});
const [isLoading, setLoading] = useState(true);
useEffect(() => {
getAllNodes();
}, []);
const getAllNodes = () => {
axios.get("http://localhost:5001/").then((response) => {
setNodes(response.data);
setLoading(false);
});
};
if (isLoading) {
return <div className="App">Loading...</div>;
}
return (
<>
<Container allNodes={nodes} />
</>
);
}
Calling the render function before the API call is finished is fine. The wells is an empty array (initial state), you simply render nothing. And after receiving the data from API, your component will automatically re-render because the update of props (redux store). So I don't see the problem.
If you really want to prevent it from rendering before receiving API data, just check that in your render function, for example:
if (this.props.wells.length === 0) {
return null
}
return (
<div id="map" ref="map">
{this.renderMarkers()}
</div>
)
So I have the similar problem, with react and found out solution on my own. by using Async/Await calling react
Code snippet is below please try this.
import Loader from 'react-loader-spinner'
constructor(props){
super(props);
this.state = {loading : true}
}
getdata = async (data) => {
return await data;
}
getprops = async (data) =>{
if (await this.getdata(data)){
this.setState({loading: false})
}
}
render() {
var { userInfo , userData} = this.props;
if(this.state.loading == true){
this.getprops(this.props.userData);
}
else{
//perform action after getting value in props
}
return (
<div>
{
this.state.loading ?
<Loader
type="Puff"
color="#00BFFF"
height={100}
width={100}
/>
:
<MyCustomComponent/> // place your react component here
}
</div>
)
}