I added like and unlike buttons to my react app. I'm using redux to manage the state and storing the data in firebase realtime-database. The buttons are working as they should but I need to reload the page to show the post has been liked/unliked, it is not re-rendering on its own. I tried using both forceUpdate and setState but both didn't work.
postLiked = (id) => {
this.props.onLikePost(this.props.user, id)
this.forceUpdate()
}
postUnliked = (id, unlikeID) => {
this.props.onUnlikePost(id, unlikeID)
}
render() {
{this.props.data.map((res) => {
const liked = [];
for(let key in res.LikedBy){
liked.push({
...res.LikedBy[key],
id: key
});
}
let boolButton = false;
if(liked.filter((val) => {
if(val.username === this.props.user) {
boolButton = true
}
}))
return(
<div>
<div className="bottomButtons">
{boolButton ? <Button className="btn btn-primary likeDislike"
id="likeButton"
onClick={() => this.postUnliked(res.id, liked.find((val) => {
if(val.username === this.props.user){
return val.id;
}
}))}
>
<FontAwesomeIcon icon={faThumbsUp} style={{width:"13.5px", color:"white"}}/>
</Button> : <Button className="btn btn-light likeDislike"
id="likeButton"
onClick={() => this.postLiked(res.id)}
>
<FontAwesomeIcon icon={faThumbsUp} style={{width:"13.5px"}}/>
</Button>
}
These are the action functions
export const likePost = (username, postId) => {
const req = {
username,
postId
}
return (dispatch) => {
axios.post('/Data/' + postId + '/LikedBy.json', req)
.then((res) => {
dispatch({
type: actionTypes.Like_Post,
payload: res
})
})
}
}
export const unlikePost = (id, unlikeId) => {
return (dispatch) => {
axios.delete('/Data/' + id + '/LikedBy/' + unlikeId.id + '.json')
.then((res) => {
dispatch({
type: actionTypes.Unlike_Post
})
}).catch((error) => {
console.log(error)
})
}
}
And this is the reducer function
const initialState = {
Data: []
}
const reducer = (state = initialState, action) => {
switch(action.type){
case actionTypes.Like_Post:
return {
...state,
Data: state.Data.map((post) => post.id === action.payload.postId
? {...post, LikedBy: post.LikedBy.concat(action.payload.username)}:post),
loading: false,
}
case actionTypes.Unlike_Post:
return {
...state,
Data: state.Data,
loading: false,
}
EDIT
I tried other methods but nothing is working. The issue is with the reducer and I am not correctly updating the state. I tried updating the LikedBy field but I only get an error.
Tried this approach but I got an error saying res.map is not a function
case actionTypes.Like_Post:
return {
...state,
Data: state.Data.forEach((res) => res.map((q) => {
if(q.id === action.payload.postId) {
q.LikedBy.concat(action.payload.username)
}
return q
})
)
}
You shouldn't be re-loading the page to upload this state really (don't listen to people who tell you to do it); this is one of the many problems that React was designed to solve. The reason you are having an issue is because your component isn't SEEING a change to its data therefor a re-render is not being triggered, this is most likely because you aren't passing the correct prop into your component.
Your redux reducer is referring to Data but locally in your component you are using this.props.data try making sure you are actually passing your data reducer properly into the component.
Related
I have an issue with redux and probably useEffect(I am not sure where my mistake is). I am trying to get information from PokeAPI and store information in the redux state. The problem is that the information about pokemons don't include pokemon types(fire, water, etc.), to solve this I am sending requests to fetch those types from a different endpoint and I want to include these types of specific pokemon to redux state.
1-redux state without types of pokemons
2-redux state with types of pokemons
My goal is to have a state like in the second picture with types. But when I refresh the page, I only acquire the first picture(actions aren't dispatching). When I change something in my code and save it, I get types as well. I suspect that my problem is in the useEffect, but I couldn't find a solution without creating some nasty loops.
Note: Page parameter in fetchData coming from PokeAPI, it basically returns 15 pokemon for every page.(For now I am just experimenting on the first page)
This is my first question in stackoverflow, I already searched for similar questions but those were dealing with different aspects, so I decided to ask myself.
PokemonList.js --> this is where I need those types
import React, { useEffect } from 'react';
import { ListGroup, ListGroupItem } from "react-bootstrap";
import { useDispatch, useSelector } from 'react-redux';
import _ from "lodash";
import { GetPokemonList, GetSpecificPokemon } from '../redux/actions/PokemonAction';
import { Button } from 'react-bootstrap';
const PokemonList = () => {
const dispatch = useDispatch();
const pokemonList = useSelector(state => state.PokemonList);
useEffect(() => {
const fetchData = (page = 1) => {
dispatch(GetPokemonList(page));
}
fetchData();
}, [dispatch]);
useEffect(() => {
const fetchTypes = () => {
pokemonList.data.forEach(pokemon => {
dispatch(GetSpecificPokemon(pokemon.name));
});
}
fetchTypes();
}, [dispatch]);
const showData = () => {
if (!_.isEmpty(pokemonList.data)) {
return (
<div className="pokemon-list-wrapper">
{pokemonList.data.map((pokemon, index) => {
return (
<div className="pokemon-list-element" key={index}>
<ListGroup>
<ListGroupItem action href={`/pokemon/${pokemon.name}`} variant="success">{pokemon.name}
<Button style={{ float: "right" }}>Test</Button>
</ListGroupItem>
</ListGroup>
</div>
)
})}
</div>
)
}
if (pokemonList.loading) {
return <p>Loading...</p>
}
if (pokemonList.errorMessage !== "") {
return <p>{pokemonList.errorMessage}</p>
}
};
return (
<div>
{showData()}
</div>
)
};
export default PokemonList;
PokemonAction.js
import axios from "axios"
export const GetPokemonList = (page) => async (dispatch) => {
try {
dispatch({
type: "POKEMON_LIST_LOADING"
});
const perPage = 15;
const offset = (page * perPage) - perPage;
const res = await axios.get(`https://pokeapi.co/api/v2/pokemon?limit=${perPage}&offset=${offset}`);
dispatch({
type: "POKEMON_LIST_SUCCESS",
payload: res.data
});
} catch (e) {
dispatch({
type: "POKEMON_LIST_FAIL"
});
}
}
export const GetSpecificPokemon = (name) => async (dispatch) => {
try {
const res = await axios.get(`https://pokeapi.co/api/v2/pokemon/${name}`);
dispatch({
type: "SPECIFIC_POKEMON_SUCCESS",
payload: res.data
});
} catch (e) {
dispatch({
type: "SPECIFIC_POKEMON_FAIL"
});
}
}
PokemonListReducer.js
const initialState = {
data: [],
loading: false,
errorMessage: "",
count: 0
};
const PokemonListReducer = (state = initialState, action) => {
switch (action.type) {
case "POKEMON_LIST_LOADING":
return {
...state,
loading: true,
errorMessage: ""
};
case "POKEMON_LIST_FAIL":
return {
...state,
loading: false,
errorMessage: "unable to get pokemon"
};
case "POKEMON_LIST_SUCCESS":
return {
...state,
loading: false,
data: action.payload.results,
errorMessage: "",
count: action.payload.count
};
case "SPECIFIC_POKEMON_SUCCESS":
const typesMap = action.payload.types.map((type) => {
return type.type.name;
})
return {
...state,
data: state.data.map((pokemon) => pokemon.name === action.payload.name
? {...pokemon, types: typesMap}
: pokemon
),
loading: false,
errorMessage: ""
}
case "SPECIFIC_POKEMON_FAIL":
return {
...state,
loading: false,
errorMessage: "unable to get pokemon"
};
default:
return state
}
}
export default PokemonListReducer;
This is happening because your second useEffect does not wait for your first useEffect to finish and because of that the pokemon list is empty. On code change, since the state already has the pokemon list pre-filled, the second useEffect finds the list and does it's thing. You have to guarantee that the second action is caller right after the first one in order for this to work properly. One way to do this is to dispatch the GetSpecificPokemon action for each pokemon before finishing the GetPokemonList action. Something like this should work:
export const GetPokemonList = (page) => async (dispatch) => {
try {
dispatch({
type: "POKEMON_LIST_LOADING"
});
const perPage = 15;
const offset = (page * perPage) - perPage;
const res = await axios.get(`https://pokeapi.co/api/v2/pokemon?limit=${perPage}&offset=${offset}`);
dispatch({
type: "POKEMON_LIST_SUCCESS",
payload: res.data
});
res.data.result.forEach(pokemon => {
dispatch(GetSpecificPokemon(pokemon.name));
});
} catch (e) {
dispatch({
type: "POKEMON_LIST_FAIL"
});
}
}
Note that you won't be needing the second useEffect if you are doing this. You might also have to change displaying/not displaying the loader part yourself.
Another way is to add pokemonList as the second object in the useEffect's array parameter. I haven't tested it yet but this should work. For example:
useEffect(() => {
const fetchTypes = () => {
pokemonList.data.forEach(pokemon => {
dispatch(GetSpecificPokemon(pokemon.name));
});
}
fetchTypes();
}, [dispatch, pokemonList]);
This will call the useEffect whenever there is a change in pokemonList. In your implementation, useEffect is only called once since the value of dispatch never really changes after that. Adding pokemonList to the array results in the useEffect being called when there is a change in pokemonList also. Use this approach if you want the GetPokemonList action to always be separate from GetSpecificPokemon action i.e there are cases when both are not called together. If both are always called together then the first approach is cleaner.
That being said, these implementations actually result in a lot of network calls. The best way is to avoid the second call if possible (change your UI accordingly?) since you do not have any control over the API. If you do have control over the API you could include the extra data in the first request's response.
Edit: Here is the batch logic
const p = pokemonList.map(({ name }) =>
axios.get(`https://pokeapi.co/api/v2/pokemon/${name}`)
);
const res = await Promise.all(p);
const data = res.map((r) => ({
...r.data,
types: r.data.types.map((type) => type.type.name) // the logic you were using for types
}));
dispatch({
type: "SPECIFIC_POKEMON_SUCCESS",
payload: data
});
And then update the state in the reducer like
case "SPECIFIC_POKEMON_SUCCESS":
return {
...state,
data: action.payload,
loading: false,
errorMessage: ""
};
I'm building the MERN eCommerce app, and I'm facing a weird UI bug with pulling data from Redux Store.
After switching pages from one product page to another, there is the old product image shown and then updated.
As you can see: https://imgur.com/iU9TxJr
There you can see code for reducer:
export const productDetailsReducer = (
state = { product: { reviews: [] } },
action
) => {
switch (action.type) {
case PRODUCT_DETAILS_REQUEST:
return { loading: true, ...state };
case PRODUCT_DETAILS_SUCCESS:
return { loading: false, product: action.payload };
case PRODUCT_DETAILS_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
And also code for action:
export const listProductDetails = (id) => async (dispatch) => {
try {
dispatch({ type: PRODUCT_DETAILS_REQUEST });
const { data } = await axios.get(`/api/products/${id}`);
dispatch({
type: PRODUCT_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: PRODUCT_DETAILS_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
And lastly, there is component's code where I bringing redux store into the page
const ProductPage = ({ match }) => {
const dispatch = useDispatch();
const productDetails = useSelector((state) => state.productDetails);
const { loading, error, product } = productDetails;
useEffect(() => {
dispatch(listProductDetails(match.params.id));
}, [dispatch, match]);
console.log(product);
return (
<>
{loading ? (
<Loader />
) : error ? (
<Message variant="danger">{error}</Message>
) : (
<Row>
<Col md={8}>
<Image src={product.image} alt={product.name} fluid />
</Col>
...rest of component's code....
I know probably would be the best to not even use redux for a single product, but I'm using this project for practice, and I'm kinda stuck at this one.
Thanks to everyone!
What you need to do is clear the selected product when you leave the detail page. You can do that using the return function of useEffect. So probably something like:
useEffect(() => {
dispatch(listProductDetails(match.params.id));
return () => {
dispatch(clearSelectedProduct());
}
}, [dispatch, match]);
And add corresponding action and reducer changes..
case CLEAR_SELECTED_PRODUCT:
return { loading: false, product: { reviews: [] } };
This way, when you arrive on the product detail page, the previous product is always cleared.
I'm building an eCommerce Store with React + Redux + Firebase. The problem I'm facing is that the Shopcomponent is rendered properly when routet on (e.g. clicking on link in nav bar), but when the page is reached through a direct URL or a page refresh, the images of the articles won't show up.
The Shop page fetches data of the articles from the Firestore and a downloadable URL of the images from the Cloud Storage.
Below you will find the code of my App component that fetches the described data from the Firestore and Storage, and dispatches this data to the Redux store. It also contains the Route to the Shop component.
const App = props => {
const dispatch = useDispatch();
useEffect(() => {
const storageRef = storage.ref();
const arr = [];
dispatch(fetchProductsPending());
async function fetchData() {
try {
await firestore
.collection("necklaces")
.get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
// doc.data() is never undefined for query doc snapshots
arr.push(doc.data());
console.log("is")
});
})
const asyncForEach = async (array, callback) => {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array)
}
}
//Async forEach loop
const start = async () => {
await asyncForEach(arr, async (item) => {
console.log(item)
await storageRef.child(`images/${item.name}/1.png`).getDownloadURL().then(function(url) {
return (
item.url = url
)
});
console.log(item.url)
})
}
start();
dispatch(fetchProductsSuccess(arr));
} catch (err) {
dispatch(fetchProductsError(err));
}
}
fetchData();
}, [])
return (
<Route path="/shop" render={({props}) => (
<MainLayout>
<Shop {...props}/>
</MainLayout>
)}>
</Route>
Here the ShopPage that consumes the information from the Redux store:
const mapState = ({ products }) => ({
products: products.products,
pending: products.pending
});
const Shop = props => {
const { products, pending } = useSelector(mapState);
return (
<div className="row">
{products.map((item, index) => {
console.log(item)
return (
<ShopItems
name={item.name}
price={item.price}
key={item.id}
id={item.id}
url={item.url}
/>
);
})}
And the ShopItems that receive the props (the redux state) from the Shopcomponent:
const ArticleItems = props => {
return (
<div className="article">
<Link className="items" to={`/article/${props.id}`}>
<img className="itemPicture" src={props.url} alt=""/>
<h6>{props.name}</h6>
<h6>{props.price}€</h6>
</Link>
</div>
)
}
And just in case my Redux code if necessary:
Actions
export function fetchProductsPending() {
return {
type: productTypes.FETCH_PRODUCTS_PENDING
}
}
export function fetchProductsSuccess(products) {
return {
type: productTypes.FETCH_PRODUCTS_SUCCESS,
payload: products
}
}
export function fetchProductsError(error) {
return {
type: productTypes.FETCH_PRODUCTS_ERROR,
payload: error
}
}
Reducer
const INITIAL_STATE = {
pending: false,
products: [],
error: null
}
export function productsReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case productTypes.FETCH_PRODUCTS_PENDING:
return {
...state,
pending: true
}
case productTypes.FETCH_PRODUCTS_SUCCESS:
return {
...state,
pending: false,
products: action.payload
}
case productTypes.FETCH_PRODUCTS_ERROR:
return {
...state,
pending: false,
error: action.error
}
default:
return state;
}
}
Please help, I'm loosing my mind over this and I can't get it right. Thank you!
In the "Favorite List" reducer
I have two helper function "Add/Remove" item from the array
the Add work well but Remove it does not update the store in the actual time, because I have a checker in my UI that checks if this song_id in the array or not and bassed on it I update the heart icon BUT it does not work well when I dispatch the remove Action, In Other Words "Not Re-render the component"!.
Action File
import {ADD_TO_FAVORITE, REMOVE_FROM_FAVORITE} from './types';
export const addToFavoriteFunction = track_id => {
return {
type: ADD_TO_FAVORITE,
payload: track_id,
};
};
export const removeFromFavoriteFunction = track_id => {
return {
type: REMOVE_FROM_FAVORITE,
payload: track_id,
};
};
Reducer
import {ADD_TO_FAVORITE, REMOVE_FROM_FAVORITE} from '../actions/types';
let initialState = [];
const addSongFav = (songs, songId, flag) => {
if (songs.some(song => song.track_id === songId)) {
return songs;
} else {
let isFav = {track_id: songId, isFavorite: flag};
return [...songs, isFav];
}
};
const removeSongFav = (songs, songId) => {
const newState = songs.filter(song => song.track_id !== songId);
return newState;
};
const isFavoriteReducer = (state = initialState, action) => {
const {payload, type} = action;
switch (type) {
case ADD_TO_FAVORITE: {
return addSongFav(state, payload, true);
}
case REMOVE_FROM_FAVORITE:
return removeSongFav(state, payload);
default:
return state;
}
};
export default isFavoriteReducer;
"Music Player Component"
....
checkFavorite = () => {
let {currentTrackIndex, tunes} = this.state;
console.log(tunes[currentTrackIndex].id);
let id = tunes[currentTrackIndex].id;
let songs = this.props.favorite;
let isFavorite = songs.some(song => song.track_id === id);
this.setState({isFavorite});
};
componentDidMount() {
this.checkFavorite();
}
addToFavorite = async () => {
const {tunes, token, currentTrackIndex} = this.state;
this.setState({isFavorite: true});
let id = tunes[currentTrackIndex].id;
try {
this.props.addToFavoriteAction(id);
let AuthStr = `Bearer ${token}`;
const headers = {
'Content-Type': 'application/json',
Authorization: AuthStr,
};
// here i send a hit the endoint
} catch (err) {
this.setState({isFavorite: false});
console.log(err);
}
};
deleteFromFavorite = async () => {
const {tunes, token, isFavorite, currentTrackIndex} = this.state;
let id = tunes[currentTrackIndex].id;
this.props.removerFromFavoriteAction(id);
try {
let AuthStr = `Bearer ${token}`;
const headers = {
'Content-Type': 'application/json',
Authorization: AuthStr,
};
// here i send a hit the endoint
} catch (err) {
console.log(err);
}
};
<Button onPress={() => this.state.isFavorite
? this.deleteFromFavorite()
: this.addToFavorite()} >
<Icon name={this.state.isFavorite ? 'favorite' : 'favorite-border'} />
</Button>
....
const mapDispatchToProps = dispatch => {
return {
incrementCount: count => {
dispatch(incrementCount(count));
},
addToFavoriteAction: track_id => {
dispatch(addToFavoriteFunction(track_id));
},
removerFromFavoriteAction: track_id => {
dispatch(removeFromFavoriteFunction(track_id));
},
};
};
mapStateToProps = state => {
return {
favorite: state.favorite,
};
};
export default connect(mapStateToProps, mapDispatchToProps)(MusicPlayer);
Thanks for the live demo, it helped a lot to see the whole picture. The issue is that your view is not actually using the values in your Redux store at all. The reducer is fine and everything is working behind the scenes, but take a look...
const mapStateToProps = state => {
return {
favorite: state,
};
};
This is your mapStateToProps method, and favorite contains an array of the favorite tracks that is successfully being updated whenever you dispatch an action. The reason why your view is not updated accordingly is that you're not using this array anywhere.
<Icon
style={{color:"#00f"}}
type="MaterialIcons"
name={this.state.isFavorite ? 'favorite' : 'favorite-border'}
/>
In this piece of code, what you're checking is the value of a isFavorite property inside of your component's inner state. The reason why it works when you add a favorite is because you're calling setState at the beginning of addToFavorite. On the contrary, deleteFromFavorite is missing that setState call, which is the reason your icon is not changing.
If you want to use what you have in the Redux store to determine which icon to show, you should change your code so it uses this.props.favorite, which is the property that actually references the store and changes according to your actions.
const isCurrentTrackFavorite = () => {
const { tunes, currentTrackIndex } = this.state;
const currentTrackId = tunes[currentTrackIndex].track_id;
// Check array in the Redux store to see if the track has been added to favorites
return this.props.favorite.findIndex(track => track.track_id === currentTrackId) != -1;
};
render() {
<Icon
style={{color:"#00f"}}
type="MaterialIcons"
name={isCurrentTrackFavorite() ? 'favorite' : 'favorite-border'}
/>
}
By making this change, your component will be really listening to the contents of the store and should update the view whenever the array of favorites changes.
(Only my 3rd post here, so please excuse any blatant issues).
The following is my Unit component, a child of a Course component (courses has_many units).
import React from 'react';
import { connect } from 'react-redux';
import { getUnits, addUnit, updateUnit } from '../reducers/units';
import { Container, Header, Form } from 'semantic-ui-react';
class Units extends React.Component {
initialState = { name: ''}
state = { ...this.initialState }
componentDidUpdate(prevProps) {
const { dispatch, course } = this.props
if (prevProps.course.id !== course.id)
dispatch(getUnits(course.id))
}
handleSubmit = (e) => {
debugger
e.preventDefault()
debugger
const unit = this.state
const { dispatch } = this.props
if (unit.id) {
debugger
dispatch(updateUnit(unit))
} else {
debugger
dispatch(addUnit(unit))
this.setState({ ...this.initialState })
}
}
handleChange = (e) => {
const { name, value } = e.target
this.setState({ [name]: value })
}
units = () => {
return this.props.units.map( (unit, i) =>
<ul key={i}>
<li key={unit.id}> {unit.name}</li>
<button>Edit Module Name</button>
<button>Delete Module</button>
</ul>
)
}
render() {
const { name } = this.state
return (
<Container>
<Header as="h3" textAlign="center">Modules</Header>
{ this.units() }
<button>Add a Module</button>
<Form onSubmit={this.handleSubmit}>
<Form.Input
name="name"
placeholder="name"
value={name}
onChange={this.handleChange}
label="name"
required
/>
</Form>
</Container>
)
}
}
const mapStateToProps = (state) => {
return { units: state.units, course: state.course }
}
export default connect(mapStateToProps)(Units);
The following is its reducer:
import axios from 'axios';
import { setFlash } from './flash'
import { setHeaders } from './headers'
import { setCourse } from './course'
const GET_UNITS = 'GET_UNITS';
const ADD_UNIT = 'ADD_UNIT';
const UPDATE_UNIT = 'UPDATE_UNIT';
export const getUnits = (course) => {
return(dispatch) => {
axios.get(`/api/courses/${course}/units`)
.then( res => {
dispatch({ type: GET_UNITS, units: res.data, headers: res.headers })
})
}
}
export const addUnit = (course) => {
return (dispatch) => {
debugger
axios.post(`/api/courses/${course}/units`)
.then ( res => {
dispatch({ type: ADD_UNIT, unit: res.data })
const { headers } = res
dispatch(setHeaders(headers))
dispatch(setFlash('Unit added successfully!', 'green'))
})
.catch( (err) => dispatch(setFlash('Failed to add unit.', 'red')) )
}
}
export const updateUnit = (course) => {
return (dispatch, getState) => {
const courseState = getState().course
axios.put(`/api/courses/${course.id}/units`, { course })
.then( ({ data, headers }) => {
dispatch({ type: UPDATE_UNIT, course: data, headers })
dispatch(setCourse({...courseState, ...data}))
dispatch(setFlash('Unit has been updated', 'green'))
})
.catch( e => {
dispatch(setHeaders(e.headers))
dispatch(setFlash(e.errors, 'red'))
})
}
}
export default (state = [], action) => {
switch (action.type) {
case GET_UNITS:
return action.units;
case ADD_UNIT:
return [action.unit, ...state]
case UPDATE_UNIT:
return state.map( c => {
if ( c.id === action.unit.id )
return action.unit
return c
})
default:
return state;
}
};
Note: My reducer is working for my getUnits and rendering the units properly.
Note also: when I try to submit a new unit, it ignores all of the debuggers in my handleSubmit and the debuggers in my addUnits (in the reducer), but somehow renders the flash message of "Failed to add units".
Then the console logs the error seen in the title of this post.
I raked my routes and my post is definitely supposed to go to the route as it is.
I have tried passing in the unit and the course in various ways without any change to the error.
How can it hit the flash message without hitting any of the debuggers?
How do I fix this [object%20Object]issue?
Thanks in advance!
The variable course in the following line
axios.get(`/api/courses/${course}/units`)
is an object. When you try to convert an object to a string in JavaScript, [object Object] is the result. The space is then converted to %20 for the URL request.
I would look at the contents of the course variable. Likely, what you actually want in the URL is something inside of course. Perhaps course.id.
If you are still having issues, you'll need to explain what value should go in the URL between /courses/ and /units, and where that data exists.
You are invoking addUnit and updateUnit with a parameter that is equal to this.state in handleSubmit
const unit = this.state
addUnit(unit)
As this.state is of type object, it is string concatenated as object%20object.
getUnit works fine as the parameter passed there comes from the prop course. Check the value of state inside handleSubmit.