How to #connect to async loaded data? - asynchronous

I'm writing an app with React, Redux, react-redux, react-router, react-router-redux and redux-async-connect. The special part of the app is that all client-server communication is done over websockets.
My users can read/write several walls, that I store is a walls store with the following reducer and basic helper functions:
const LOAD = 'ws/wall/LOAD';
const LOAD_SUCCESS = 'estisia/wall/LOAD_SUCCESS';
const LOAD_FAIL = 'estisia/wall/LOAD_FAIL';
const ADD_MESSAGE = 'estisia/wall/ADD_MESSAGE';
const initialWallState = {
loaded: false,
messages: []
};
const initialState = {
walls: {}
};
function wall(state = initialWallState, action = {}) {
switch (action.type) {
... // omitted for brevity
default:
return state;
}
}
export default function walls(state = initialState, action = {}) {
if (typeof action.data === 'undefined' || typeof action.data.wallId === 'undefined') return state;
const newState = {
...state.walls,
[action.data.wallId]: wall(state.walls[action.data.wallId], action)
};
return {walls: newState};
}
export function isLoaded(wallId, globalState) {
return globalState.walls.wallId && globalState.walls.wallId.loaded;
}
export function load(wallId) {
return {
type: LOAD,
send: ['wall/messages/page', [wallId, 1]]
};
}
and I have a Wall container where the appropriate wallId is passed in by react-router, but can't figure out how to make the #connect call pass only the relevant wall data to the container. What should I do instead of the #connect meta-code below to get it working?
#asyncConnect([{
deferred: true,
promise: ({store: {dispatch, getState}, params}) => {
if (!isLoaded(params.wallId, getState())) {
return dispatch(loadWall(params.wallId));
}
}
}])
#connect(
state => ({
messages: state.wall[this.params.wallId].messages,
loading: state.wall[this.params.wallId].loading
}))
export default class Wall extends Component {
static propTypes = {
messages: PropTypes.array
}
... // omitted for brevity
}
How can I achieve redux-async-connect to help me out with the above case?

Related

I cannot understand WHY I cannot change state in Redux slice

I get the array of objects coming from backend, I get it with socket.io-client. Here we go!
//App.js
import Tickers from "./Components/TickersBoard";
import { actions as tickerActions } from "./slices/tickersSlice.js";
const socket = io.connect("http://localhost:4000");
function App() {
const dispatch = useDispatch();
useEffect(() => {
socket.on("connect", () => {
socket.emit("start");
socket.on("ticker", (quotes) => {
dispatch(tickerActions.setTickers(quotes));
});
});
}, [dispatch]);
After dispatching this array goes to Action called setTickers in the slice.
//slice.js
const tickersAdapter = createEntityAdapter();
const initialState = tickersAdapter.getInitialState();
const tickersSlice = createSlice({
name: "tickers",
initialState,
reducers: {
setTickers(state, { payload }) {
payload.forEach((ticker) => {
const tickerName = ticker.ticker;
const {
price,
exchange,
change,
change_percent,
dividend,
yeild,
last_trade_time,
} = ticker;
state.ids.push(tickerName);
const setStatus = () => {
if (ticker.yeild > state.entities[tickerName].yeild) {
return "rising";
} else if (ticker.yeild < state.entities[tickerName].yeild) {
return "falling";
} else return "noChange";
};
state.entities[tickerName] = {
status: setStatus(),
price,
exchange,
change,
change_percent,
dividend,
yeild,
last_trade_time,
};
return state;
});
return state;
},
},
});
But the state doesn't change. I tried to log state at the beginning, it's empty. After that I tried to log payload - it's ok, information is coming to action. I tried even to do so:
setTickers(state, { payload }) {
state = "debag";
console.log(state);
and I get such a stack of logs in console:
debug
debug
debug
3 debug
2 debug
and so on.

Passing variables from middleware to page in Next.js 12 new middleware api

Background to the Question
Vercel recently released their biggest update ever to Next.js. Next.js blog.
They introduced a lot of new features but my favorite is Middleware which:
"enables you to use code over configuration. This gives you full
flexibility in Next.js because you can run code before a request is
completed. Based on the user's incoming request, you can modify the
response by rewriting, redirecting, adding headers, or even streaming
HTML."
The Question
The following structure is used in this question.
- /pages
index.js
signin.js
- /app
_middleware.js # Will run before everything inside /app folder
index.js
The two important files here are /app/_middleware.js and /app/index.js.
// /app/_middleware.js
import { NextResponse } from 'next/server';
export function middleware(req, event) {
const res = { isSignedIn: true, session: { firstName: 'something', lastName: 'else' } }; // This "simulates" a response from an auth provider
if (res.isSignedIn) {
// Continue to /app/index.js
return NextResponse.next();
} else {
// Redirect user
return NextResponse.redirect('/signin');
}
}
// /app/index.js
export default function Home() {
return (
<div>
<h1>Authenticated!</h1>
// session.firstName needs to be passed to this file from middleware
<p>Hello, { session.firstName }</p>
</div>
);
}
In this example /app/index.js needs access to the res.session JSON data. Is it possible to pass it in the NextResponse.next() function or do you need to do something else?
In express you can do res.locals.session = res.session
According to the examples (look specifically at /pages/_middleware.ts and /lib/auth.ts) it looks like the canonical way to do this would be to set your authentication via a cookie.
In your middleware function, that would look like:
// /app/_middleware.js
import { NextResponse } from 'next/server';
export function middleware(req, event) {
const res = { isSignedIn: true, session: { firstName: 'something', lastName: 'else' } }; // This "simulates" a response from an auth provider
if (res.isSignedIn) {
// Continue to /app/index.js
return NextResponse.next().cookie("cookie_key", "cookie_value"); // <--- SET COOKIE
} else {
// Redirect user
return NextResponse.redirect('/signin');
}
}
There's a another way but just like using cookie to achieve this. Just pass you data through headers.
// middleware.ts
async function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('X-HEADER', 'some-value-to-pass');
return response;
}
// _app.ts
function MyApp({ data }) {
// you can access your data here
<div>{data}</div>
}
MyApp.getInitialProps = ({ ctx }) => {
const data = ctx.res.getHeader('X-HEADER');
ctx.res.removeHeader('X-HEADER');
return { data };
};
Only weird solution is to inject your custom object into req.body because next.js v12 middleware doesn't allow altering the NextApiRequest
export const middleware = async (req: NextApiRequest) => {
// return new Response("Hello, world!");
req.body = { ...req.body, foo: "bar" };
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
await middleware(req);
// now req.body.foo=='bar'
}
They do however explain how you can extend middleware here, but the example given (copied below) isn't meaningful enough because it doesnt show how withFoo() is implemented
import { NextApiRequest, NextApiResponse } from 'next'
import { withFoo } from 'external-lib-foo'
type NextApiRequestWithFoo = NextApiRequest & {
foo: (bar: string) => void
}
const handler = (req: NextApiRequestWithFoo, res: NextApiResponse) => {
req.foo('bar') // we can now use `req.foo` without type errors
res.end('ok')
}
export default withFoo(handler)
I assumed based on the above, withFoo.ts should be like this. But still wasn't successful in accessing request.Foo()
import { NextApiHandler, NextApiRequest } from "next";
export const withFoo = (handler: NextApiHandler) => {
//do stuff
};
Maybe someone can chip in?
We found a solution for 12.2+ middleware - published here:
https://clerk.dev/blog/nextjs-pass-value-from-middleware-to-api-routes-and-getserversideprops
And copying here for posterity...
Usage: middleware.js
import { NextResponse } from "next/server";
import { withContext } from "./context";
// Pre-define the possible context keys to prevent spoofing
const allowedContextKeys = ["foo"];
export default withContext(allowedContextKeys, (setContext, req) => {
setContext("foo", "bar");
return NextResponse.next();
});
Usage: API route (Node)
import { getContext } from "../../context";
export default function handler(req, res) {
res.status(200).json({ foo: getContext(req, "foo") });
}
Usage: API route (Edge)
import { getContext } from "../../context";
export default function handler(req) {
return new Response(JSON.stringify({ foo: getContext(req, "foo") }));
}
Usage: getServerSideProps (Edge and Node)
import { getContext } from "../context";
export const getServerSideProps = ({ req }) => {
return { props: { foo: getContext(req, "foo") } };
};
Source: (saved to context.js on your root)
import { NextResponse } from "next/server";
const ctxKey = (key) => `ctx-${key.toLowerCase()}`;
export const getContext = (req, rawKey) => {
const key = ctxKey(rawKey);
let headerValue =
typeof req.headers.get === "function"
? req.headers.get(key) // Edge
: req.headers[key]; // Node;
// Necessary for node in development environment
if (!headerValue) {
headerValue = req.socket?._httpMessage?.getHeader(key);
}
if (headerValue) {
return headerValue;
}
// Use a dummy url because some environments only return
// a path, not the full url
const reqURL = new URL(req.url, "http://dummy.url");
return reqURL.searchParams.get(key);
};
export const withContext = (allowedKeys, middleware) => {
// Normalize allowed keys
for (let i = 0; i < allowedKeys.length; i++) {
if (typeof allowedKeys[i] !== "string") {
throw new Error("All keys must be strings");
}
allowedKeys[i] = ctxKey(allowedKeys[i]);
}
return (req, evt) => {
const reqURL = new URL(req.url);
// First, make sure allowedKeys aren't being spoofed.
// Reliably overriding spoofed keys is a tricky problem and
// different hosts may behave different behavior - it's best
// just to safelist "allowedKeys" and block if they're being
// spoofed
for (const allowedKey of allowedKeys) {
if (req.headers.get(allowedKey) || reqURL.searchParams.get(allowedKey)) {
throw new Error(
`Key ${allowedKey.substring(
4
)} is being spoofed. Blocking this request.`
);
}
}
const data = {};
const setContext = (rawKey, value) => {
const key = ctxKey(rawKey);
if (!allowedKeys.includes(key)) {
throw new Error(
`Key ${rawKey} is not allowed. Add it to withContext's first argument.`
);
}
if (typeof value !== "string") {
throw new Error(
`Value for ${rawKey} must be a string, received ${typeof value}`
);
}
data[key] = value;
};
let res = middleware(setContext, req, evt) || NextResponse.next();
// setContext wasn't called, passthrough
if (Object.keys(data).length === 0) {
return res;
}
// Don't modify redirects
if (res.headers.get("Location")) {
return res;
}
const rewriteURL = new URL(
res.headers.get("x-middleware-rewrite") || req.url
);
// Don't modify cross-origin rewrites
if (reqURL.origin !== rewriteURL.origin) {
return res;
}
// Set context directly on the res object (headers)
// and on the rewrite url (query string)
for (const key in data) {
res.headers.set(key, data[key]);
rewriteURL.searchParams.set(key, data[key]);
}
// set the updated rewrite url
res.headers.set("x-middleware-rewrite", rewriteURL.href);
return res;
};
};

Redux: TypeError: e is undefined

https://github.com/reduxjs/redux/issues/3017
Problem: Occurs when I wrap my action creator with a dispatch in the container area where I utilize the connect method--I followed the style from redux documentation.
I am utilizing redux, and redux thunk. I am attempting to create a login action, so far it does not work when I dispatch an action, which dispatch's an another one.
LoginContainer.js
import CONFIG from "../../../config";
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import {authenticateUser} from "../../../actions/authenticateUser";
import Login from '../../../components/views/login/Login'
import {store} from '../../../store';
function handleSubmit(e) {
e.preventDefault();
let calpersId = parseInt(e.target[0].value || e.target[1].value, 10) || 0;
store.dispatch(authenticateUser(calpersId))
}
const mapStateToProps = (state) => {
return {
authentication: state.authentication
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleSubmit: (e) => {dispatch(handleSubmit(e))}
}
}
const LoginContainer = connect(mapStateToProps, mapDispatchToProps)(Login);
export default LoginContainer;
authenticateUser.action.js
import CONFIG from '../config'
export const AUTHENTICATE_USER = 'AUTHENTICATE_USER'
export const initiateUserAuthentication = (token) => ({
type: AUTHENTICATE_USER,
token
})
export const AUTHENTICATATION_SUCCEEDED = 'AUTHENTICATATION_SUCCEEDED'
export const authenticatationSucceeded = (payload) => ({
type: AUTHENTICATE_USER,
payload
})
export const USER_ID_DOES_NOT_EXIST = 'USER_ID_DOES_NOT_EXIST'
export const userIdDoesNotExist = (uid) => ({
type: USER_ID_DOES_NOT_EXIST,
uid,
message: "User id does not exist"
})
export function authenticateUser(id) {
return function (dispatch) {
let guidMap = {
7103503579: "dad08fde-0ac1-404a-ba8a-cc7c76d5810f",
6632408185: "6632408185-guid",
6581985123: "6581985123-guid",
1226290314: "a3908aa7-c142-4752-85ea-3741cf28f75e",
4618604679: "4618604679-guid",
6452522440: "6452522440-guid",
3685610572: "3685610572-guid",
5564535492: "5564535492-guid",
5600493427: "5600493427-guid",
3996179678: "3996179678-guid",
7302651964: "7302651964-guid",
3148148090: "3148148090-guid",
5826752269: "5826752269-guid",
6827859055: "6827859055-guid",
1677401305: "1677401305-guid",
2640602392: "dbed1af6-0fc9-45dc-96a3-ab15aa05a7a2",
6474994805: "6474994805-guid"
};
let guid = guidMap[id]
return fetch(CONFIG.API.MY_CALPERS_SERVER.LOCATION + 'ept/development/rest/simulatedAuth.json?guid=' + guid, {
credentials: 'include'
})
.then(
response => response.json(),
error => console.log('An error occured.', error))
.then(json => {
document.cookie = "authentication=" + guid + "; max-age=" + (60 * 30);
dispatch(authenticatationSucceeded(json))
})
}
}
authenticateUser.reducer.js
import {AUTHENTICATE_USER, AUTHENTICATATION_SUCCEEDED} from "../actions/authenticateUser";
const initialState = {
calpersIds: [
5600493427,
6474994805,
6452522440,
5564535492,
6632408185,
4618604679,
5826752269,
3996179678,
7302651964,
1677401305,
6827859055,
3685610572,
6581985123,
3148148090
],
guidMap: {
7103503579: "dad08fde-0ac1-404a-ba8a-cc7c76d5810f",
6632408185: "6632408185-guid",
6581985123: "6581985123-guid",
1226290314: "a3908aa7-c142-4752-85ea-3741cf28f75e",
4618604679: "4618604679-guid",
6452522440: "6452522440-guid",
3685610572: "3685610572-guid",
5564535492: "5564535492-guid",
5600493427: "5600493427-guid",
3996179678: "3996179678-guid",
7302651964: "7302651964-guid",
3148148090: "3148148090-guid",
5826752269: "5826752269-guid",
6827859055: "6827859055-guid",
1677401305: "1677401305-guid",
2640602392: "dbed1af6-0fc9-45dc-96a3-ab15aa05a7a2",
6474994805: "6474994805-guid"
},
authToken: null,
isAuthenticated: false
};
//#TODO: All fetches, create a seperate reducer for store?
export function authenticateUser(state = initialState, action) {
switch(action.type) {
case AUTHENTICATE_USER:
return Object.assign({}, state, {
authToken: action.token,
})
case AUTHENTICATATION_SUCCEEDED:
return Object.assign({}, state, {
authToken: action.payload.guid,
isAuthenticated: true,
payload: action.payload
})
default:
return state;
}
};
You should'nt use connect mapDispatchToProps like you are doing.
This callback is supposed to create or use functions that will dispatch an action.
For your case you can use it like that:
const mapDispatchToProps = (dispatch) => {
return {
authenticate: calpersId => authenticateUser(calpersId)(dispatch)
}
}
And in your component have a function/method that handle the submit:
class Login extends Component {
...
handleSubmit = e => {
e.preventDefault();
const calpersId = parseInt(e.target[0].value || e.target[1].value, 10) || 0;
this.props.authenticate(calpersId)
}
...
By the way a reducer is supposed to represent the state of an entity. An entity named autenticateUser is pretty ambigious. You should propably named it user. You should read more redux examples to really catch the concept that at first a bit complicated to understand. There are good videos on Youtube.
Turns out I was calling an action creator which did not exist, I simply needed to pass my dispatch to the handler, and let it handle the the event.
Login.js
import CONFIG from "../../../config";
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import {authenticateUser} from "../../../actions/authenticateUser";
import Login from '../../../components/views/login/Login'
function handleSubmit(e, dispatch) {
e.preventDefault();
let calpersId = parseInt(e.target[0].value || e.target[1].value, 10) || 0;
dispatch(authenticateUser(calpersId))
}
const mapStateToProps = (state) => {
return {
authentication: state.authentication
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleSubmit: (e) => {handleSubmit(e, dispatch)}
}
}
const LoginContainer = connect(mapStateToProps, mapDispatchToProps)(Login);
export default LoginContainer;
What is the proper way of doing this, I utillized bindActionCreators which yields the same result.

Redux combineReducer returns default state for reducer not called in action

I'm new to react redux, so I think I'm just missing something basic.
I have three reducers, two to handle orders that update in the store as arrays, and one that shows the status of a web socket connection I'm using to receive orders from the server.
// reducers.js
import { combineReducers } from 'redux'
import { ADD_POS_ORDER, ADD_MOBILE_ORDER, UPDATE_WS_STATUS, wsStatuses } from '../actions/actions'
const { UNINITIALIZED } = wsStatuses
const posOrders = (state = [], action) => {
switch (action.type) {
case ADD_POS_ORDER:
return [
...state,
{
id: action.order.id,
status: action.order.status,
name: action.order.name,
pickupNum: action.order.pickupNum
}
]
default:
return state
}
}
const mobileOrders = (state = [], action) => {
switch (action.type) {
case ADD_MOBILE_ORDER:
return [
...state,
{
id: action.order.id,
status: action.order.status,
name: action.order.name,
pickupNum: action.order.pickupNum
}
]
default:
return state
}
}
const wsStatus = (state = UNINITIALIZED, action) => {
switch (action.type) {
case UPDATE_WS_STATUS:
return action.status
default:
return state
}
}
const displayApp = combineReducers({
posOrders,
mobileOrders,
wsStatus
})
export default displayApp
When I connect to the socket, I dispatch an action to update wsStatus and the action is stored as 'CONNECTED'.
When I follow with an order with the posOrders reducer, the wsStatus is reset to its default, 'UNINITIALIZED'.
What I am struggling to understand is why wsStatus is not using the previous state of 'CONNECTED', but instead returning default.
// actions.js
export const UPDATE_WS_STATUS = 'UPDATE_WS_STATUS'
export const wsStatuses = {
UNINITIALIZED: 'UNINITIALIZED',
CONNECTING: 'CONNECTING',
CONNECTED: 'CONNECTED',
DISCONNECTED: 'DISCONNECTED'
}
export const ADD_POS_ORDER = 'ADD_POS_ORDER'
export const ADD_MOBILE_ORDER = 'ADD_MOBILE_ORDER'
export const UPDATE_POS_ORDER = 'UPDATE_POS_ORDER'
export const setWsStatus = (status) => {
return {
type: 'UPDATE_WS_STATUS',
status: status
}
}
export const updateOrderQueue = (action, order) => {
return {
type: action,
id: order.id,
order: order,
receivedAt: Date.now()
}
}
Here's where I make the calls:
// socketListeners.js
import { setWsStatus } from '../actions/actions'
import SockJS from 'sockjs-client'
export const socket = new SockJS('http://localhost:3000/echo')
export default function (dispatch, setState) {
socket.onopen = function () {
dispatch(setWsStatus('CONNECTED'))
}
socket.onclose = function () {
dispatch(setWsStatus('DISCONNECTED'))
}
}
// orders container
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { socket } from '../helpers/socketListeners'
import { updateOrderQueue, setWsStatus } from '../actions/actions'
import PosOrder from '../components/queue/PosOrder'
class PosOrderList extends Component {
constructor(props) {
super(props)
}
componentDidMount() {
const { dispatch } = this.props
socket.onmessage = function(e) {
// convert order info to object
let parsedOrder = JSON.parse(e.data)
let action = parsedOrder.action
let order = parsedOrder.order
dispatch(updateOrderQueue(action, order))
}
}
render() {
const { updateOrderQueue } = this.props
return (
<ul>
{this.props.posOrders.map(posOrder =>
<PosOrder
key={posOrder.id}
{...posOrder}
/>
)}
</ul>
)
}
}
PosOrderList.propTypes = {
posOrders: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.hash,
status: PropTypes.string,
name: PropTypes.string,
pickupNum: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
}))
}
// send data to component props
const mapStateToProps = (state) => {
return {
posOrders: state.posOrders,
}
}
export default connect(mapStateToProps)(PosOrderList)
// store
const store = configureStore(initialState)
export default function configureStore(initialState) {
return createStore(
displayApp,
initialState,
applyMiddleware(
createLogger({
stateTransformer: state => state.toJS()
}),
thunk,
// socketMiddleware
)
)
}
addSocketListeners(store.dispatch, store.getState)
Lastly, the store logs here: redux store
Any and all help on this would be very appreciated! Thank you!
When you compose your reducer with combineReducers, for each dispatched action, all subreducers get invoked, since every reducer gets a chance to respond to every action.
Therefore, all state gets initialized after the first action is dispatched.
Your reducers are working fine https://jsfiddle.net/on8v2z8j/1/
var store = Redux.createStore(displayApp);
store.subscribe(render);
store.dispatch({type: 'UPDATE_WS_STATUS',status:'CONNECTED'});
store.dispatch({type: 'ADD_POS_ORDER',id:'id'});
store.dispatch({type: 'UPDATE_WS_STATUS',status:'DISCONNECTED'});

redux not picking up an object dispatched via actions

I created a rootSaga in sagas.js as
function* fetchStuff(action) {
try {
yield put({type: 'INCREMENT'})
yield call(delay, 1000)
yield put({type: 'DECREMENT'})
const highlights = yield call(API.getStuff, action.data.myObject);
} catch (e) {
yield put({type: 'FETCH_STUFF_FAILED', message: e});
}
}
export default function* rootSaga() {
yield takeEvery('INIT_LOAD', fetchStuff);
}
I am calling the INIT_LOAD after thirdParty.method:
class myClass extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.load();
}
load = () => {
this.init = () => {
this.myObject = thirdParty.method(event => {
const action = {
type: 'INIT_LOAD',
payload: {
myObject: this.myObject
}
};
store.dispatch(action);
});
};
this.init();
};
render() {
return (
<div id="render-here" />
);
}
Passing the this.myObject in the action that is dispatched does not trigger the saga. If I change the action payload to a string, like the following, the saga is triggered.
const action = {
type: 'INIT_LOAD',
payload: {
myObject: 'this.myObject'
}
};
Why am I unable to pass this.myObject but a string is ok?
UPDATE: It is not a saga issue. I replicated the same issue with just plain redux. The rootReducer as
export default function rootReducer(state = initialState, action) {
switch (action.type) {
case 'INIT_LOAD':
return Object.assign({}, state, { myObject: action.payload.myObject });
default:
return state;
}
}
As I mentioned in the comment below, assigning it to an object Obj does not change the issue
let Obj = {};
...
load = () => {
this.init = () => {
Obj.myObject = thirdParty.method(event => {
const action = {
type: 'INIT_LOAD',
payload: {
myObj: Obj
}
};
store.dispatch(action);
});
};
this.init();
};
UPDATE2
I cleaned the code up & simply dispatched an action in the component that triggers the saga. Inside the saga is where I do the init(). I ran into another issue where the object that I was trying to save in the redux store has active socket sessions (which were given me cross-domain issues). Although I didn't solve my original problem, not storing a socket object made my problem go away.

Resources