I'm working a side project using react, redux and firebase as backend.
Right now I was looking a way to store the URL of the image I was uploading into my firestore database along side other data.
This is what is working at the moment:
Action addProduct.js
export const addProduct = (product) => {
return (dispatch, getState, { getFireStore }) => {
const db = firestore;
const uploadTask = storage.ref(`images/${product.image.name}`).put(product.image);
uploadTask.on(
"state_changed",
snapshot => {},
error => {
console.log(error);
},
() => {
storage
.ref("images")
.child(product.image.name)
.getDownloadURL()
.then(url => {
let updatedProduct = {
...product,
image: url
}
db.collection('products').add({
...updatedProduct,
}).then(() => {
dispatch({
type: ADD_PRODUCT,
updatedProduct
})
}).catch((err) => {
dispatch({ type: ADD_PRODUCT_ERR, err })
})
})
}
)
}
Couple of things bugging me here:
While it successfully uploads the image and stores its url in the database, this code looks really messy from my perspective. Can an action file be this complex? Is there a more cleaner way to do this?
My reducer is returning an undefined payload for some reason I can't find.
Reducer productReducer.js
import { ADD_PRODUCT, ADD_PRODUCT_ERR } from "../actions/types"
const initialState = {
product: null
}
export default (state = initialState, action) => {
switch(action.type) {
case ADD_PRODUCT:
console.log('Product Added', action.product)
return state
case ADD_PRODUCT_ERR:
console.log('Product Failed', action.err)
return state
default:
return state
}
}
Related
This is my jsonApiFetchOiAndLTPs reducer
export const jsonApiFetchOiAndLTPs = createAsyncThunk('exchange/jsonApiFetchOiAndLTPs', async (payload, { dispatch, getState }) => {
const { account, exchange, filters, registry } = getState()
const appliedFilters = payload === undefined ? filters : payload?.filters
console.log('payload', payload); // here payload always being undefined
console.log('appliedFilters', appliedFilters);
// remaining code goes here
}
In one of my file i am dispatching action as shwon below
useEffect(() => {
if(open) {
localStorageData?.forEach(item => {
const specKey = contractSpecKey(item)
if(!contracts.hasOwnProperty(specKey)) {
const appliedFilters = {
tokenPair: item.tokenPair,
optionStyle: item.optionStyle,
contractType: item.contractType,
expiryDate: item.expiryDate,
lotSize: item.lotSize
}
dispatch(jsonApiFetchOiAndLTPs({filters: appliedFilters}))
// here i am dispatching the action
}
})
}
}, [open])
For each items of localstorage data i am dispatching action based on if consition
I'm trying to keep session stayed logged in after refreshing the browser. The user data that is being fetched is not rendering after being fetched. The console is saying "Cannot read properties of undefined (reading 'user'). This is my code for the login/sign up page.
The data I'm trying to access is in the picture below:
(Auth.js)
const Auth = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const [isSignup, setIsSignup] = useState(false);
const [inputs, setInputs] = useState({
name: "",
username: "",
email: "",
password: ""
})
const handleChange = (e) => {
setInputs(prevState => {
return {
...prevState,
[e.target.name]: e.target.value
}
})
}
const sendRequest = async (type = '') => {
const res = await axios.post(`/user/${type}`, {
name: inputs.name,
email: inputs.email,
username: inputs.username,
password: inputs.password,
}).catch(error => console.log(error))
const data = await res.data;
console.log(data)
return data;
}
const handleSubmit = (e) => {
e.preventDefault()
console.log(inputs)
if (isSignup) {
sendRequest("signup")
.then((data) => {
dispatch(authActions.login());
localStorage.setItem('userId', data.user._id);
navigate("/posts");
});
} else {
sendRequest("login")
.then((data) => {
dispatch(authActions.login());
localStorage.setItem('userId', data.user._id);
navigate("/posts");
});
}
}
Redux store file
const authSlice = createSlice({
name: "auth",
initialState: { isLoggedIn: false },
reducers: {
login(state) {
state.isLoggedIn = true
},
logout(state) {
state.isLoggedIn = false
}
}
})
export const authActions = authSlice.actions
export const store = configureStore({
reducer: authSlice.reducer
})
Chaining promises using .then() passes the resolved value from one to the next. With this code...
sendRequest("...")
.then(() => dispatch(authActions.login()))
.then(() => navigate("/posts"))
.then(data => localStorage.setItem('token', data.user))
You're passing the returned / resolved value from navigate("/posts") to the next .then() callback. The navigate() function returns void therefore data will be undefined.
Also, your redux action doesn't return the user so you can't chain from that either.
To access the user data, you need to return it from sendRequest()...
const sendRequest = async (type = "") => {
try {
const { data } = await axios.post(`/user/${type}`, { ...inputs });
console.log("sendRequest", type, data);
return data;
} catch (err) {
console.error("sendRequest", type, err.toJSON());
throw new Error(`sendRequest(${type}) failed`);
}
};
After that, all you really need is this...
sendRequest("...")
.then((data) => {
dispatch(authActions.login());
localStorage.setItem('userId', data.user._id);
navigate("/posts");
});
Since you're using redux, I would highly recommend moving the localStorage part out of your component and into your store as a side-effect.
I have a function "sendMessage" in React class:
class MessageForm extends React.Component {
...
sendMessage = async () => {
const { message } = this.state;
if (message) {
this.setState({ loading: true });
if (this.props.isPrivateChannel === false) {
socket.emit("createMessage", this.createMessage(), (response) => {
this.setState({ loading: false, message: "", errors: [] });
});
} else {
if (this.state.channel && this.state.channel._id === undefined) {
socket.emit("createChannelPM", this.state.channel, async (response) => {
const chInfo = { ...response, name: this.props.currentChannel.name };
console.log("chInfo : ", chInfo);
await this.props.setCurrentChannel(chInfo).then((data) => {
if (data) {
console.log("data : ", data);
console.log("this.props.currentChannel : ", this.props.currentChannel);
}
});
});
}
...
function mapStateToProps(state) {
return {
isPrivateChannel: state.channel.isPrivateChannel,
currentChannel: state.channel.currentChannel,
};
}
const mapDispatchToProps = (dispatch) => {
return {
setCurrentChannel: async (channel) => await dispatch(setCurrentChannel(channel)),
}
};
Here, in sendMessage function, I retrieve "response" from socket.io, then put this data into variable "chInfo" and assign this to Redux state, then print it right after assinging it.
And Redux Action function, "setCurrentChannel" looks like:
export const setCurrentChannel = channel => {
return {
type: SET_CURRENT_CHANNEL,
payload: {
currentChannel: channel
}
};
};
Reducer "SET_CURRENT_CHANNEL" looks like:
export default function (state = initialState, action) {
switch (action.type) {
case SET_CURRENT_CHANNEL:
return {
...state,
currentChannel: action.payload.currentChannel
};
...
The backend Socket.io part look like (I use MongoDB):
socket.on('createChannelPM', async (data, callback) => {
const channel = await PrivateChannel.create({
...data
});
callback(channel)
});
The console.log says:
Problem : The last output, "this.props.currentChannel" should be same as the first output "chInfo", but it is different and only print out previous value.
However, in Redux chrome extension, "this.props.currentChannel" is exactly same as "chInfo":
How can I get and use newly changed Redux states immediately after assinging it to Redux State?
You won't get the updated values immediately in this.props.currentChannel. After the redux store is updated mapStateToProps of MessageForm component is called again. Here the state state.channel.currentChannel will be mapped to currentChannel. In this component you get the updated props which will be accessed as this.props.currentChannel.
I believe you want to render UI with the latest data which you which you can do.
I have a like function on the backend (Node, MongoDB) that returns the given post with updated likes counter. This works, tested it with Postman. This is just an object with a bunch of properties like likes, _id., by, createdAt and so on...
let p = await Post.findById(req.params.id).populate("by");
return res.json(p);
Then I have a like action in React:
export const like = (id) => (dispatch) => {
const token = localStorage.getItem("token");
if (token) {
axios
.put(`http://localhost:5000/likePost/${id.id}`, id, {
headers: { "X-Auth-Token": token },
})
.then((res) => {
dispatch({
type: LIKE,
payload: res.data,
});
});
}
};
And I have a LIKE reducer:
case LIKE:
return {
...state,
posts: state.posts.map((p) => {
return { ...p };
}),
};
The LIKE reducer triggers when I click on the button and on the backend I can see the update but on the client side it doesn't update. I use redux-logger and the posts state is not updated.
What did I do wrong? I thought that spreading all the posts (...p) will update it, since it is updated on the backend.
This one works:
case LIKE:
return {
...state,
posts: state.posts.map((p) => {
if (p._id === action.payload._id) {
p.likes = action.payload.likes;
}
return p;
}),
};
I have been following these testing guidelines to test my vuex store.
But when I touched upon the actions part, I felt there is a lot going on that I couldn't understand.
The first part goes like:
// actions.js
import shop from '../api/shop'
export const getAllProducts = ({ commit }) => {
commit('REQUEST_PRODUCTS')
shop.getProducts(products => {
commit('RECEIVE_PRODUCTS', products)
})
}
// actions.spec.js
// use require syntax for inline loaders.
// with inject-loader, this returns a module factory
// that allows us to inject mocked dependencies.
import { expect } from 'chai'
const actionsInjector = require('inject!./actions')
// create the module with our mocks
const actions = actionsInjector({
'../api/shop': {
getProducts (cb) {
setTimeout(() => {
cb([ /* mocked response */ ])
}, 100)
}
}
})
I infer that this is to mock the service inside the action.
The part which follows is:
// helper for testing action with expected mutations
const testAction = (action, payload, state, expectedMutations, done) => {
let count = 0
// mock commit
const commit = (type, payload) => {
const mutation = expectedMutations[count]
expect(mutation.type).to.equal(type)
if (payload) {
expect(mutation.payload).to.deep.equal(payload)
}
count++
if (count >= expectedMutations.length) {
done()
}
}
// call the action with mocked store and arguments
action({ commit, state }, payload)
// check if no mutations should have been dispatched
if (expectedMutations.length === 0) {
expect(count).to.equal(0)
done()
}
}
describe('actions', () => {
it('getAllProducts', done => {
testAction(actions.getAllProducts, null, {}, [
{ type: 'REQUEST_PRODUCTS' },
{ type: 'RECEIVE_PRODUCTS', payload: { /* mocked response */ } }
], done)
})
})
This is where it I find it difficult to follow.
My store looks like:
import * as NameSpace from '../NameSpace'
import { ParseService } from '../../Services/parse'
const state = {
[NameSpace.AUTH_STATE]: {
auth: {},
error: null
}
}
const getters = {
[NameSpace.AUTH_GETTER]: state => {
return state[NameSpace.AUTH_STATE]
}
}
const mutations = {
[NameSpace.AUTH_MUTATION]: (state, payload) => {
state[NameSpace.AUTH_STATE] = payload
}
}
const actions = {
[NameSpace.ASYNC_AUTH_ACTION]: ({ commit }, payload) => {
ParseService.login(payload.username, payload.password)
.then((user) => {
commit(NameSpace.AUTH_MUTATION, {auth: user, error: null})
})
.catch((error) => {
commit(NameSpace.AUTH_MUTATION, {auth: [], error: error})
})
}
}
export default {
state,
getters,
mutations,
actions
}
And This is how I am trying to test:
import * as NameSpace from 'src/store/NameSpace'
import AuthStore from 'src/store/modules/authorization'
const actionsInjector = require('inject!../../../../../src/store/modules/authorization')
// This file is present at: test/unit/specs/store/modules/authorization.spec.js
// src and test are siblings
describe('AuthStore Actions', () => {
const injectedAction = actionsInjector({
'../../Services/parse': {
login (username, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve({})
} else {
reject({})
}
}, 300)
})
}
}
})
it('Gets the user profile if the username and password matches', () => {
const testAction = (action, payload, state, mutations, done) => {
const commit = (payload) => {
if (payload) {
expect(mutations.payload).to.deep.equal(payload)
}
}
action({ commit, state }, payload)
.then(result => {
expect(state).to.deep.equal({auth: result, error: null})
})
.catch(error => {
expect(state).to.deep.equal({auth: [], error: error})
})
}
testAction(injectedAction.login, null, {}, [])
})
})
If I try to do this, I get:
"Gets the user profile if the username and password matches"
undefined is not a constructor (evaluating 'action({ commit: commit, state: state }, payload)')
"testAction#webpack:///test/unit/specs/store/modules/authorization.spec.js:96:13 <- index.js:26198:14
webpack:///test/unit/specs/store/modules/authorization.spec.js:104:15 <- index.js:26204:16"
I need help understanding what am I supposed to do to test such actions.
I know it's been awhile but I came across this question because I was having a similar problem. If you were to console.log injectedActions right before you make the testAction call you'd see that the injectedAction object actually looks like:
Object{default: Object{FUNC_NAME: function FUNC_NAME(_ref) { ... }}}
So the main solution here would be changing the testAction call to:
testAction(injectedAction.default.login, null, {}, [], done)
because you are exporting your action as defaults in your store.
A few other issues that are unrelated to your particular error... You do not need to manipulate the testAction boilerplate code. It will work as expected so long as you pass in the proper parameters. Also, be sure to pass done to testAction or your test will timeout. Hope this helps somebody else who comes across this!